Stopwatch for Pinewood Derby

OK, you guys got me going again–I do love Pinewood Derby!

The laser goes across the track at the starting line and is a beam-break system. We used this one from Microwizard.

The photo transistor end is fixed rigidly but the laser end is held on by a magnet and you can slide it up/down/left/right to get the beam in the hole.

The slot in the track in front of each car has a dowel that is attached to the starting bar. All of the dowels rotate out of the way at the time. The first car to start down the track and break the laser beam starts the timer. You would think that first car to break the beam always wins but that is definitely not the case since there are two phases to race, the steep downhill accelerating phase and the flatter coasting phase. Many races are won in the flatter coasting phase.

As I said earlier, we initially had the starting system where rotating the starting bar hit a microswitch to start the race. This does not work as well for a variety of reasons.

The length of each lane is identical. Bumping the center guide strip will slow you down so having the wheels aligned perfectly with just a little toe-in for stability helps a lot. The finish line sensor works the way Tim described with a slot in the track except instead of proximity sensors, it was beam-break with LEDs above. The Sharp proximity sensors are nice and would work great.

+1 on using micros() and turning off interrupts.

Hey @sweenig Your Pack should have a rule book that says how the race is run; it should be available in advance. Not having this is a recipe for angry parents in my experience! The rule book should say how the race is timed so everyone knows what is going to happen.

I have run district wide races with the winners from many Packs and it is very competitive–amongst the parents that is! We also used fitting boxes made to the specs in the rule book so that the car had to fit completely in the box with nothing outside except in the vertical direction (+z). And of course accurate scales and a test weight. To avoid angry parents (“well it weighed in OK at our Pack!”) the test weight goes on the scale before every car that gets weighed to check the scale.

Running a Pack race is fun, but running a District wide race felt more like work.

Alright, so I’m leaning toward 1 LED/photoresistor pair (or possibly 4 pairs) up top and 4 LED/photoresistor pairs of at the bottom. The top could be triggered by the first car to pass the beam or could be triggered by the release handle (handle would probably be more fair since it’s a static piece and guaranteed to happen at the same time for every car). That takes care of the physical layout.

So, it sounds like the code would loop through using micros() and reading the entire port register. Mode A would be to wait for the top pin’s edge, which would grab a timestamp and enter Mode B. Mode B would be waiting for the edges of the four bottom pins’ edges, which would grab a timestamp and enter Mode C. Mode C would report the deltas and wait for a button push to enter Mode A (waiting for the race to start again). Sound reasonable?

@timb I’d love to see the demo code you mention. Sounds like it should be pretty easy to mold into what I need.

I still think interrupts would be faster.

Polling the port would also require you to mask off each bit and know when a bit changes. If a change is detected, you save the micros() counter associated to a Race Lane. While you are going through each bit, 1, 2, 3, 4, a previous lane may trigger but you don’t detect it because you were off looking at another bit. Because of this I don’t know how reading the entire port at once helps.

The interrupts would catch all edges, first thing and only thing you do in the interrupt service routine is save the micros() count into an appropriate global variable associated with the race lane. In your main loop you can take your time looping and when you detect all 4 race lanes have end times, you can enter your Mode C. You also can have a fail safe timeout running in your main loop that prevents a stuck car from messing up the reporting of the other lanes, say after 10 seconds.

http://docs.spark.io/#/firmware/interrupts-attachinterrupt

Pretty sure the last time I looked it seemed like we could attach at least 4 interrupts at once… but probably more. Does anybody know for sure how many? If we still can’t save the micros() count inside an ISR, we’ll need to figure that out.

Read code comments :wink:

#include "application.h"

void blink(void);
int ledPin = D7;
volatile int state = LOW;

void setup()
{
  // attach interrupts to any of these pins  
  // D0, D1, D2, D3, D4
  // A0, A1, A3, A4, A5, A6, A7
  pinMode(ledPin, OUTPUT);
  
  // works for any combination of the first 7 shown here, 
  // if you also add A3, D2 stopped working.  If you enable
  // them all at the same time only 5 or 6 of them work.
  pinMode(D0, INPUT_PULLUP);
  pinMode(D1, INPUT_PULLUP);
  pinMode(D2, INPUT_PULLUP);
  pinMode(D3, INPUT_PULLUP);
  pinMode(D4, INPUT_PULLUP);
  pinMode(A0, INPUT_PULLUP);
  pinMode(A1, INPUT_PULLUP);
  //pinMode(A3, INPUT_PULLUP);
  //pinMode(A4, INPUT_PULLUP);
  //pinMode(A5, INPUT_PULLUP);
  //pinMode(A6, INPUT_PULLUP);
  //pinMode(A7, INPUT_PULLUP);
  
  attachInterrupt(D0, blink, CHANGE);
  attachInterrupt(D1, blink, CHANGE);
  attachInterrupt(D2, blink, CHANGE);
  attachInterrupt(D3, blink, CHANGE);
  attachInterrupt(D4, blink, CHANGE);
  attachInterrupt(A0, blink, CHANGE);
  attachInterrupt(A1, blink, CHANGE);
  //attachInterrupt(A3, blink, CHANGE);
  //attachInterrupt(A4, blink, CHANGE);
  //attachInterrupt(A5, blink, CHANGE);
  //attachInterrupt(A6, blink, CHANGE);
  //attachInterrupt(A7, blink, CHANGE);
}

void loop()
{
  digitalWrite(ledPin, state);
}

void blink()
{
  state = !state;
}

Ok here’s some code that kind of works… lol. I think there may be an issue with the noInterrupts() / interrupts() functions. Or the way I’m implementing things… The first results seem good, but then the more you run it the more it flakes a bit. Some of the start/end times get very short like some bouncing of the inputs is re-triggering. I’m not seeing how unless interrupts are queuing up and firing again as soon as interrupts() is run… Not sure, but feel free to play. I’ll keep looking at it:

#include "application.h"

void lane1(void);
void lane2(void);
void lane3(void);
void lane4(void);
int ledPin = D7;
volatile int state = LOW;
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;

void setup()
{
  Serial.begin(115200);
  while(!Serial.available()) SPARK_WLAN_Loop();

  Serial.println("Waiting for all four inputs to CHANGE...\n");
  timeStart1 = micros(); // reset!
  timeStart2 = timeStart1;
  timeStart3 = timeStart1;
  timeStart4 = timeStart1;
  timeEnd1 = timeStart1;
  timeEnd2 = timeStart1;
  timeEnd3 = timeStart1;
  timeEnd4 = timeStart1;

  // D0, D1, D2, D3, D4
  // A0, A1, A3, A4, A5, A6, A7
  pinMode(ledPin, OUTPUT);
  
  pinMode(D0, INPUT_PULLUP);
  pinMode(D1, INPUT_PULLUP);
  pinMode(D2, INPUT_PULLUP);
  pinMode(D3, INPUT_PULLUP);
  //pinMode(D4, INPUT_PULLUP);
  //pinMode(A0, INPUT_PULLUP);
  //pinMode(A1, INPUT_PULLUP);
  
  //pinMode(A3, INPUT_PULLUP);
  //pinMode(A4, INPUT_PULLUP);
  //pinMode(A5, INPUT_PULLUP);
  //pinMode(A6, INPUT_PULLUP);
  //pinMode(A7, INPUT_PULLUP);
  
  attachInterrupt(D0, lane1, FALLING);
  attachInterrupt(D1, lane2, FALLING);
  attachInterrupt(D2, lane3, FALLING);
  attachInterrupt(D3, lane4, FALLING);
  //attachInterrupt(D4, blink, CHANGE);
  //attachInterrupt(A0, blink, CHANGE);
  //attachInterrupt(A1, blink, CHANGE);
  
  //attachInterrupt(A3, blink, CHANGE);
  //attachInterrupt(A4, blink, CHANGE);
  //attachInterrupt(A5, blink, CHANGE);
  //attachInterrupt(A6, blink, CHANGE);
  //attachInterrupt(A7, blink, CHANGE);
}

void loop()
{
  digitalWrite(ledPin, state);

  // still needs a timeout...
  if(timeEnd1 != timeStart1 && timeEnd2 != timeStart2 && timeEnd3 != timeStart3 && timeEnd4 != timeStart4) {
  	
  	Serial.println("======= Lane Times in seconds =======");
  	Serial.print("Lane 1: "); 
  	Serial.print(timeEnd1); Serial.print(" "); 
  	Serial.print(timeStart1); Serial.print(" "); Serial.println((double)(timeEnd1 - timeStart1)/1000000.0,6);

  	Serial.print("Lane 2: "); 
  	Serial.print(timeEnd2); Serial.print(" "); 
  	Serial.print(timeStart2); Serial.print(" "); Serial.println((double)(timeEnd2 - timeStart2)/1000000.0,6);

  	Serial.print("Lane 3: "); 
  	Serial.print(timeEnd3); Serial.print(" "); 
  	Serial.print(timeStart3); Serial.print(" "); Serial.println((double)(timeEnd3 - timeStart3)/1000000.0,6);

  	Serial.print("Lane 4: "); 
  	Serial.print(timeEnd4); Serial.print(" "); 
  	Serial.print(timeStart4); Serial.print(" "); Serial.println((double)(timeEnd4 - timeStart4)/1000000.0,6);
  	
  	// prevent false triggering from last input bouncing while we reset
  	noInterrupts();
  	delay(5000); // this is much longer than it needs to be... 
  	timeStart1 = micros(); // reset!
  	timeStart2 = timeStart1;
  	timeStart3 = timeStart1;
  	timeStart4 = timeStart1;
  	timeEnd1 = timeStart1;
  	timeEnd2 = timeStart1;
  	timeEnd3 = timeStart1;
  	timeEnd4 = timeStart1;
  	interrupts(); // go!
  	Serial.println("\n\nWaiting for all four inputs to CHANGE...\n");
  	
  }
}

void lane1()
{
  // if not captured already, save time.
  // this inherently debounces the input.
  if(timeEnd1 == timeStart1) timeEnd1 = micros();
  state = !state;
}

void lane2()
{
  // if not captured already, save time.
  // this inherently debounces the input.
  if(timeEnd2 == timeStart2) timeEnd2 = micros();
  state = !state;
}

void lane3()
{
  // if not captured already, save time.
  // this inherently debounces the input.
  if(timeEnd3 == timeStart3) timeEnd3 = micros();
  state = !state;
}

void lane4()
{
  // if not captured already, save time.
  // this inherently debounces the input.
  if(timeEnd4 == timeStart4) timeEnd4 = micros();
  state = !state;
}

Just take a wire connected to GND and poke the D0, D1, D2, D3 holes on the breadboard to act as the trigger.

Ok! 1:41am… Pinewood Derby code keeping me up!

These interrupts and timing really don’t work like they should. I had to get in there are TAKE OVER!!! When I get some sleep I’ll explain WHY I had to do what I did. EDIT: I updated the header with some info… please read. Also I’ll add that at one point I tried detaching the interrupts as soon as they fired the first time, and after re-attaching them they would randomly have their Pending Registers already set and they would fire off immediately. I still would like to scrub the spark_wiring_interrupts.cpp to see if it there’s something that can fix this behavior.

This works beautifully now. You ground D2 to start the race timer. Then you ground A6, A7, D3 and D4 to capture their times and stop the race and display times. If you short A6, A7, D3 & D4 together and ground them all at the same time, you’ll notice they fire off about ~180ns apart! Definitely not going to see that precision with polling. Also, based on the winning order, you can easily adjust the times to gain even more precision!

Sample Output (inputs grounded together):

GND D2 to Start Race, GND A6, A7, D3 & D4 to End Race
=====================================================

Waiting for A6, A7, D3 & D4 to go LOW...

======= Lane Times in seconds =======
Lane 1: 2.89765485
Lane 2: 2.89765671
Lane 3: 2.89766039
Lane 4: 2.89765853

GND D2 to Start Race, GND A6, A7, D3 & D4 to End Race
=====================================================

Waiting for A6, A7, D3 & D4 to go LOW...

======= Lane Times in seconds =======
Lane 1: 1.83023608
Lane 2: 1.83023425
Lane 3: 1.83023976
Lane 4: 1.83023790

GND D2 to Start Race, GND A6, A7, D3 & D4 to End Race
=====================================================

Waiting for A6, A7, D3 & D4 to go LOW...

======= Lane Times in seconds =======
Lane 1: 3.50006700
Lane 2: 3.50006886
Lane 3: 3.50007254
Lane 4: 3.50007068

Interrupt Driven Pinewood Derby Code:

// 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);
int ledPin = D7;
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 = 10 * 1000;  // in milliseconds (10 seconds)

void setup()
{
  Serial.begin(115200);
  while(!Serial.available()) SPARK_WLAN_Loop(); // Open terminal and press ENTER

  Serial.println("GND D2 to Start Race, GND A6, A7, D3 & D4 to End Race");
  Serial.println("=====================================================\n");

  pinMode(ledPin, OUTPUT); // debug LED
  
  pinMode(D2, INPUT_PULLUP); // startRace
  pinMode(A6, INPUT_PULLUP); // lane1
  pinMode(A7, INPUT_PULLUP); // lane2
  pinMode(D3, INPUT_PULLUP); // lane3
  pinMode(D4, INPUT_PULLUP); // lane4
  
  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
  
  // Disable lanes by default, keep startRace enabled
  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()
{
  if(!raceEnded) {
    if((timeEnd1 != timeStart1 && 
       timeEnd2 != timeStart2 && 
       timeEnd3 != timeStart3 && 
       timeEnd4 != timeStart4) || 
       (millis() - startTime) > DISQUALIFIED_TIME) {

      double tempTime;
      
      Serial.println("======= Lane Times in seconds =======");
      Serial.print("Lane 1 (A6): "); 
      //Serial.print(timeEnd1); Serial.print(" "); 
      //Serial.print(timeStart1); Serial.print(" "); 
      tempTime = (double)(timeEnd1 - timeStart1)/72000000.0;
      if(tempTime != 0.0) Serial.println(tempTime,8);
      else Serial.println("DISQUALIFIED!");
      delay(50);

      Serial.print("Lane 2 (A7): "); 
      //Serial.print(timeEnd2); Serial.print(" "); 
      //Serial.print(timeStart2); Serial.print(" "); 
      tempTime = (double)(timeEnd2 - timeStart2)/72000000.0;
      if(tempTime != 0.0) Serial.println(tempTime,8);
      else Serial.println("DISQUALIFIED!");
      delay(50);

      Serial.print("Lane 3 (D3): "); 
      //Serial.print(timeEnd3); Serial.print(" "); 
      //Serial.print(timeStart3); Serial.print(" "); 
      tempTime = (double)(timeEnd3 - timeStart3)/72000000.0;
      if(tempTime != 0.0) Serial.println(tempTime,8);
      else Serial.println("DISQUALIFIED!");
      delay(50);

      Serial.print("Lane 4 (D4): "); 
      //Serial.print(timeEnd4); Serial.print(" "); 
      //Serial.print(timeStart4); Serial.print(" "); 
      tempTime = (double)(timeEnd4 - timeStart4)/72000000.0;
      if(tempTime != 0.0) Serial.println(tempTime,8);
      else Serial.println("DISQUALIFIED!");
      delay(50);
      
      raceEnded = true; // prevents results from being displayed over and over  
      EXTI_ClearITPendingBit(EXTI_Line5); // D2 "startRace"
      NVIC_EnableIRQ(EXTI9_5_IRQn); // D2
      Serial.println("\nGND D2 to Start Race, GND A6, A7, D3 & D4 to End Race");
      Serial.println("=====================================================\n");
    }
  }

  if(showStartMsg) {
    Serial.println("Waiting for A6, A7, D3 & D4 to go LOW...\n");
    showStartMsg = false;
    raceEnded = false;
    startTime = millis(); // Capture the rough start time, for disqualification timer
  }
}

void lane1()
{
  NVIC_DisableIRQ(EXTI0_IRQn); // A6
  timeEnd1 = DWT->CYCCNT; //micros();
  
  //digitalWrite(ledPin, HIGH);
  //delayMicroseconds(200000);
  //digitalWrite(ledPin, LOW);
}

void lane2()
{
  NVIC_DisableIRQ(EXTI1_IRQn); // A7
  timeEnd2 = DWT->CYCCNT; //micros();
  
  //digitalWrite(ledPin, HIGH);
  //delayMicroseconds(200000);
  //digitalWrite(ledPin, LOW);
}

void lane3()
{
  NVIC_DisableIRQ(EXTI4_IRQn); // D3
  timeEnd3 = DWT->CYCCNT; //micros();
  
  //digitalWrite(ledPin, HIGH);
  //delayMicroseconds(200000);
  //digitalWrite(ledPin, LOW);
}

void lane4()
{
  NVIC_DisableIRQ(EXTI3_IRQn); // D4
  timeEnd4 = DWT->CYCCNT; //micros();
  
  //digitalWrite(ledPin, HIGH);
  //delayMicroseconds(200000);
  //digitalWrite(ledPin, LOW);
}

void startRace()
{
  NVIC_DisableIRQ(EXTI9_5_IRQn); // D2
  timeStart1 = DWT->CYCCNT; // reset!
  timeStart2 = timeStart1;
  timeStart3 = timeStart1;
  timeStart4 = timeStart1;
  timeEnd1 = timeStart1;
  timeEnd2 = timeStart1;
  timeEnd3 = timeStart1;
  timeEnd4 = timeStart1;
  EXTI_ClearITPendingBit(EXTI_Line0); // A6 "Lane 1"
  EXTI_ClearITPendingBit(EXTI_Line1); // A7 "Lane 2"
  EXTI_ClearITPendingBit(EXTI_Line4); // D3 "Lane 3"
  EXTI_ClearITPendingBit(EXTI_Line3); // D4 "Lane 4"
  NVIC_EnableIRQ(EXTI0_IRQn); // A6 "Lane 1"
  NVIC_EnableIRQ(EXTI1_IRQn); // A7 "Lane 2"
  NVIC_EnableIRQ(EXTI4_IRQn); // D3 "Lane 3"
  NVIC_EnableIRQ(EXTI3_IRQn); // D4 "Lane 4"
  showStartMsg = true;
  
  //digitalWrite(ledPin, HIGH);
  //delayMicroseconds(200000);
  //digitalWrite(ledPin, LOW);
}
1 Like

@BDub, you are a rock star! I modified the messages a bit but in my testing it works just the way you describe. I can only think of one tweak: what if there are only 3 racers? I can see it would be easy to manually trigger the empty lane, but there should be a cool way to do it.

Here are my thoughts: turn the raceEnded variable into 4 variables, one for each lane. Turn the if(!raceEnded) branch into four branches so that the times post as soon as each car finishes. This provides 2 advantages: 1) if there are less than 4 racers, we don’t have to manually trigger anything just to get the times to display and 2) the times show up in the order that they finish.

Thoughts?

My next question is going to be how to get the spark serial output to be read into my raspberry pi, so i can post the results to apache instead of displaying them in the terminal window. Can anybody point me to the documentation on installing the spark serial driver in linux?

That sounds good, AND add a timeout so that the start of the race can be re-triggered if less than 4 cars finish.

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