Arduino Kub Kar Track Timer

track
Kub Kar Track

Every year the Cub Scouts and Beavers have an annual competition for the fastest and best looking wooden toy car. They make it from little more than a block of wood and four plastic wheels. Beaver Buggies evolve into Kub Kars for the older kids, but the principle is the same. The rules regarding maximum dimensions and weight are pretty strict, but there is ample room for cosmetics. I’ve seen perfectly rendered Hershey chocolate bars, submarines and Minecraft landscapes race down the tracks. The track is a long gravity powered run of usually three lanes, grooved so that each car does not stray.

 

This year I built an Arduino-based timer for the finish line. As with most things it took longer than I had imagined, but I’m satisfied with the results. Of course I could have kept enhancing it, but thankfully this project had a hard deadline and I was saved from myself.

track-end
The Finish Line

Three cars are released in unison at the top of the track and gravity propels them to the finish line where in 2 to 3 seconds they pass the finish line and are ranked, first, second and third. Typically this is eyeballed. However with the power of a microcontroller and a few sensors we can do better.

track-front-finished.png

Operation

track-in-actionWhen the race is started the operator presses the “release” button. The center digit counts down 3..2..1..go! The word “go!” is spelt out on the 7-segment displays which indicates to the other operator at the beginning of the track to release the cars. At this time the timer also begins, displaying seconds and milliseconds. The main light that illuminates the finish line is also activated. The first car to cross the finish line stops the timer and it blinks. Also the “go!” disappears and is replaced by the digit “1” in the lane with the winning car. As the second and third car cross the finish line the numbers “2” and “3” appear above their lanes to indicate their position. The display remains frozen until the “reset” button is pressed, at which time everything goes dark awaiting the next “release”. To prevent overheating of the light, it will automatically turn off after 30 seconds. Usually the light turns off after the third car finished, but in cases where less than three cars are racing the auto-off feature is necessary.

Enter the Arduino

At the finish line, undtrack-timer-obliqueer each lane is a light sensor. As a car passes over, darkness is detected and  a large number lights up above the lane indicating the cars’ finishing position (1st, 2nd, or 3rd).  I had originally wanted to use a laser with a lens that projected a line across all three light detectors, but it proved too cumbersome to align, so I opted for an overkill-level bright LED light to illuminate the finish line.   In addition to the three big digits I added a smaller four digit seven segment display that displays the time (seconds and milliseconds) of the winning run. A small control box has buttons to start and reset the timer.

 

 

track-scaffold
Timer Scaffold

The entire timer enclosure is elevated above the finish line by this handy scaffold that I cannot claim credit for. It was custom made to slot in right at the finish line and I though it would be perfect to mount the new Kub Kar Timer on top of. The triangular cap I made followed the angle of the feet of this scaffold. It is from there the light and laser is projected down to the track.

 

 

track-side
Timer enclosure side view

 

 

I won’t bother including specifics about building the enclosure. I figure everyone’s requirements will vary. I will lay out the details for the electronics and micro code.

Parts List

track-front-unfinished

3 x large 6.5″ seven-segment displays (Sparkfun part number COM-08530)

3 x large digit drivers (Sparkfun part number WIG-13270)

1 I2C driven 4 digit seven segment display (Adafruit part number 1270)

1 Arduino Mega 2560 R3 (Adafruit part number 191)

3 Photo Transistor Light sensors (Adafruit part number 2831)

3 5K ohm variable resistors (used to calibrate the light sensor voltage)

2 normally closed momentary push buttons (one start button and one reset button)

1 small project box (used to make the button box)

1 short (about 2 foot) length of Ethernet cable (T-568A) (connects the main timer enclosure to the light sensors embedded in the track)

Surface mount 2 keystone port enclosure (for 2 CAT5 keystone jacks) (one jack to connect to the timer box, the other to connect to the button box)

1 Wall mount single keystone port panel (can be seen in side view picture of enclosure)

3 CAT5 keystone jacks

1 bright LED light. I used a ridiculously overpowered 10W LED, but a much more modest power will suffice.

1 60V 32A N-channel MOSFET (used so that a 5v line can control a 12v circuit)

1 12v 2A AC-DC Power Supply with a 2.1mm barrel jack.

various Male and Female headers are used to avoid permanently soldering major components together. This makes assembly and diagnostics easier.

These are used to simply extend a power jack from the enclosure and plug it into the Arduino:

1 2.1mm female barrel jack power socket

1 2.1mm male barrel jack power plug

The laser looks cool but has no practical function. I tried to simply use the laser to illuminate all three lane light sensors, but it was too difficult to align them all. I include them here only for completeness.

1 Red Line Laser (Adafruit part number 1057)

1 Laser diode mount (Adafruit part number 1094)


The buttons are installed into a small project box that uses Ethernet cable to plug into one of the two jacks mounted on the track next to the finish line. A short Ethernet cable connects from the other jack on the track to the timer enclosure. All the other parts listed above reside in that enclosure.

track-inside

Even though it is possible to daisy chain the large digit drivers so that they can all be used as a single display, that was not done in this project, specifically because I wanted independent control of each digit. With an Arduino Mega and its enormous number of GPIO pins this was not a problem. However I did daisy chain the ground, 5v and 12v lines, just not the control lines.

Arduino Mega 2560 R3 Pin Use

00038-mega2

Each large display needs a CLOCK, LATCH and DATA dedicated pin on the Arduino.

  • Lane One Latch : pin 11
  • Lane One Clock: pin 12
  • Lane One Data: pin 13
  • Lane Two Latch: pin 8
  • Lane Two Clock: pin 9
  • Lane Two Data: pin 10
  • Lane Three Latch: pin 5
  • Lane Three Clock: pin 6
  • Lane Three Data: pin 7

The four-digit timer module is controlled by I2C.

  • SDA: pin 20
  • SCL: pin 21

Each lane light sensor is attached to an interrupt-capable pin. The Mega 2560 has six interrupts, but the first two also double as the SDA and SCL pins.

  • Lane One Light Sensor: pin 19 (INT2)
  • Lane Two Light Sensor: pin 18 (INT3)
  • Lane Three Light Sensor: pin 2 (INT4)

The light over the finish light is bright and heats up rather quickly. Therefore a pin was dedicated to turning the light on only when necessary. This pin goes to a MOSFET that ultimately enables or disables power to the light.

  • Light: pin 15

The optional laser serves no functional purpose, though it does make the finish line look cooler. This pin directly powers the laser.

  • Laser: pin 4

Finally the two buttons need pins of their own. One button is used to start the race (known as the release button) and the other is used to reset the board.

  • Release button: pin 3
  • Reset button: pin 14

(Pin 3 is the final interrupt pin available on the Mega, but interrupts and PC interrupts are not used for the buttons).

Connections

track-inside-closeup
Guts of the Kub Kar Timer

Power Distribution

This circuit requires both +5V and +12V in order to operate. The main power supply is 12V. This is fed directly into the Arduino via the barrel jack plug. The Arduino supplies a regulated 5V output. I used a small prototype board as a power distribution hub where ground, 5V and 12V can be distributed to the rest of the components. You can see it attached to the back wall in the picture above. From the Arduinos 5VDC pin connect a lead directly to the 5V power distribution point on the prototype board. At the point of source (the 2.1mm female barrel plug pins), tap into the +12V and lead it directly to the 12V power distribution point on the prototype board (see Timer). Do the same for the ground. Solder the ground and 5V power leads to a male header so that it can be easily plugged into the Arduino without having to make any modifications to the board.

Big Digits

The large digit drivers need to be soldered onto each of the large segment displays. Each has an IN side and OUT side that allows them to be daisy chained together. For this project we don’t need use that feature, so each display will have its own LAT, CLK and SER (latch, clock and data) lines. You will need five female headers in total. The first three will be attached to the IN side of each driver.

Power will be daisy chained among all three drivers, hence the need for two additional female headers for the OUT side on the first and second divers of the series. The following list maps out the connections from the Arduino to each of the drivers and also illustrates how power and ground is fed in from the power distribution prototype board and is daisy chained from driver to driver.

Arduino pin 5 (LAT) [male header] -->  IN right driver LAT (lane 3) [female header]
Arduino pin 6 (CLK) [male header] --> IN right driver CLK (lane 3) [female header]
Arduino pin 7 (SER) [male header] --> IN right driver SER (lane 3) [female header]
prototype board +5V [male header] --> IN right driver 5V [female header]
prototype board +12V [male header] --> IN right driver 12V [female header]
prototype board GND  [male header] --> IN right driver GND [female header]

Arduino pin 8 (LAT) [male header] --> IN middle driver LAT (lane 2) [female header]
Arduino pin 9 (CLK) [male header] --> IN middle driver CLK (lane 2)  [female header]
Arduino pin 10 (SER) [male header] --> IN middle driver SER (lane 2) [female header]
OUT right driver 5V (lane 3) [female header] -->  IN middle driver 5V (lane 2) [female header]
OUT right driver 12V (lane 3) [female header] --> IN middle driver 12V (lane 2) [female header]
OUT right driver GND (lane 3) [female header] --> IN middle driver GND (lane 2) [female header]

Arduino pin 11 (LAT) [male header] --> IN left driver LAT (lane 1) [female header]
Arduino pin 12 (CLK) [male header] --> IN left driver CLK (lane 1) [female header]
Arduino pin 13 (SER) [male header] --> IN left driver SER (lane 1) [female header]
OUT middle driver 5V (lane 3) [female header] -->  IN left driver 5V (lane 1) [female header]
OUT middle driver 12V (lane 3) [female header] --> IN left driver 12V (lane 1) [female header]
OUT middle driver GND (lane 3) [female header] --> IN left driver GND (lane 1) [female header]

13279-01

Timer

A 5 pin female header is used to connect to the four digit timer display. Use the same prototype board used for power distribution to mount the female header and break out the pins. Connect the power distribution +5V and ground points to the appropriate female header pins. Connect the SDA and SCL female header pins to leads which are soldered to male header pins that are able to connect to Arduino pins 20 and 21.

1270-01
The backside of the Timer display. Those five pins stick through to the inside of the enclosure and are matched with a five pin female header. 

Light Sensors

Each of the photo transistors are attached to the track at the finish line.  They are glued into inset holes in each lane. The light sensors are attached to a fixed and a configurable resistor (trimpot) which are also installed in the track. I installed a 5K ohm trimpot (make sure you expose the dial to allow for easy calibration).

trimpot
Trimpot

After some testing using the same light from a fixed distance, I dialled it into about 2.2K ohm. You want to make sure that the light when caught by the phototransistor produces a voltage comfortably over 3V as this is the triggering level for digital pins.  When the sensor is cast in shadow (by your hand or a passing kub kar) the voltage should drop comfortably below 3V. Since the light and distance between the track and light for each build of this project will likely be different than mine, it’s important to install trimpots to allow for calibration.

 

Since ttrack-CAT5he light sensors are embedded as part of last track unit, the 5V, ground and lane 1,2, and 3 sense lines all terminate in a CAT5 keystone jack attached to the track. From here an orange network cable connects to the main timer enclosure above (see side view picture).

KubKarTimer-LightSensor_schem

Buttons

track-buttonsThe release and reset buttons are momentary switches that default to high and only go low when pressed. They are installed in a small project box with an Ethernet CAT5 cable protruding (ending in a male RJ45 connector).  I used buttons which are “push to break” (SPST momentary, normally closed – NC) for no particular reason, but it does matter as the code would need to be modified if you use “push to make” (normally open – NC) variety instead. The cable is used for 5V, ground, release sense and reset sense.

KubKarTimer-Buttons_bb

KubKarTimer-Buttons_schem

Light

Admitted10w-high-power-led-warm-whitely a 10W LED is overkill. I had to attach a really big heatsink (salvaged from a CPU) and used thermal paste for a good thermal transfer. Additionally I added code to ensure that the light had an auto-off feature and was turned on only when the race was underway. This logic meant that the light had to be turned on and off by the microcontroller, which uses 5v logic. This particular LED required 9-12V and about 1A which was supplied directly from the 12V distribution point on the prototype board.  The N-channel MOSFET acts as an intermediary and only supplies the 12V to the light (via the drain) when the gate is high (when 5v is supplied by the Arduino). Ground is attached to the source, completing the circuit.

The MOSFET and 10K pulldown resistor are mounted to the same prototype board used for power distribution and attaches to the back of the 4-digit timer. The gate pin must attach to pin 15 of the Arduino Mega. Furthermore 12V power and ground are supplied from the power distribution points on the prototype board.

KubKarTimer-Light_bb
12V Light Controlled by a 5V Digital Out Pin. Note the rails on the protoboard are used as power distribution points.

 

 

track-protoboard

Software

Prerequisites

Be sure to set the Board in the Arduino IDE as “Mega 2560”

Be sure to install these  libraries in your Arduino IDE:

  1. Adafruit_CFX_Library
  2. Adafruit_LED_Backpack_Library

Diagnostic Mode

When the Kub Kar Tmer is initially powered up, each lane will initially display 8 and timer will display 88:88.

The laser will be turned on.

Wait.

The light will be turned on.
Each lane that detects the light will display a decimal point (.). If light is not detected, the lane will display ‘E.’ (with decimal point).

For the next 30 seconds, each available lane that passed the light detection test will attempt to detect darkness. With your hand cover each lane light sensor.
If darkness is successfully detected, the lane decimal point will disappear (so lane displays nothing).

At the end of the diagnostic each available lane will display one of the following:
‘ ‘ (nothing) – passed light and dark detection
‘E.’ (E with decimal) – failed to detect light; dark detection skipped
‘E’ (E without decimal) – passed light detection, but failed to detect dark (requires that sensor be manually covered during test).


/*
* Pinouts relate to Arduino MEGA rev 3
* Power in is 12v (needed for large segment displays)
*
* Diagnostics.
* Lanes will display ‘E.’ if there was a problem detecting light and ‘E’ if there was a problem detecting dark.
*
* On Release button push…
*
* attach ISR watchLaneOneISR() to pin LANE_ONE_LIGHT_SENSE_PIN event RISING; ISR will set laneOnePosition to 1,2 or 3
* attach ISR watchLaneTwoISR() to pin LANE_TWO_LIGHT_SENSE_PIN event RISING; ISR will set laneTwoPosition to 1,2 or 3
* attach ISR watchLaneThreeISR() to pin LANE_THREE_LIGHT_SENSE_PIN event RISING; ISR will set laneThreePosition to 1,2 or 3
* Display go!
* Turn on light.
* Record startTime.
*
* Timer displays time elapsed since startTime as long as winnerTime not set.
*
* if laneXPosition == 1 then record winnerTime and display on timer with ‘:’
* if laneXPosition > 0 then display laneXPosition over lane X
*
* after LIGHT_TIMEOUT seconds, turn off the light
*
* On Reset button push…
* Reset startTime, winnerTime, laneXPosition, laneXFinished
* detach ISR on pin LANE_ONE_LIGHT_SENSE_PIN
* detach ISR on pin LANE_TWO_LIGHT_SENSE_PIN
* detach ISR on pin LANE_THREE_LIGHT_SENSE_PIN
*
* Turn off light.
*
* — BEGIN: reset display —
* Clear timer
* Lane 1 displays ‘ ‘
* Lane 2 displays ‘ ‘
* Lane 3 displays ‘ ‘
* — END —
*
*/

//#include <gfxfont.h>
//#include <Adafruit_GFX.h> // already included by backpack, but still needs to be added to library
#include <Adafruit_LEDBackpack.h> // for i2c 4×7-segment display

#define DEBUG false

// *** Release button constants ***
#define BUTTON_CHECK_INTERVAL 20 // observe button debounce every 20ms
#define MIN_STABLE_VALS 3 // minimum number of consecutively identical button states before registering a state change for the button)
#define RELEASE_BUTTON_PIN 3 // INT5 (interrupt not used)
#define RESET_BUTTON_PIN 14 // PCINT10 (PC interrupt not used)

// *** Laser and light sensor constants ***
// The following pins are PWM capable
// (in case you wanted to detect the duty cycle of the laser if pulsed)
// Pins need to be interrupt-capable
// see https://www.arduino.cc/en/Reference/AttachInterrupt
#define LANE_ONE_LIGHT_SENSE_PIN 19 // INT2
#define LANE_TWO_LIGHT_SENSE_PIN 18 // INT3
#define LANE_THREE_LIGHT_SENSE_PIN 2 // INT4

#define LASER_PIN 4 // 20mA max output

#define LIGHT_PIN 15

/*
* Light gets hot, so limit the continuous time it is active.
*/
#define LIGHT_TIMEOUT 30000 // 30 seconds
// *** Release button variables ***
// button debounce
char stableValsRelease = 0;
char stableValsReset = 0;
unsigned long previousMillis; // initialized in setup()
boolean lastStableReleaseButtonState; // initialized in setup()
boolean lastStableResetButtonState; // initialized in setup()
// *** 4×7 Segment timer ***
Adafruit_7segment matrix = Adafruit_7segment(); // requires the use of SDA+SCL pins
unsigned long startTime; // time of release (ms since power on)
unsigned long winnerTime; // winning time (elapsed ms)
// *** Lane status variables ***
// number lane of 1st, 2nd and 3rd place finishers; 0 for none
// set and used by ISRs so make volatile
volatile byte laneOnePosition = 0;
volatile byte laneTwoPosition = 0;
volatile byte laneThreePosition = 0;

// set by loop
boolean laneOneFinished = false;
boolean laneTwoFinished = false;
boolean laneThreeFinished = false;

// *** Large lane 7-segment ***
// Each digit requires 3 pins: clock, latch and data (serial)
// These displays are not configured to cascade, so there are separate clock, latch & data pins for each
// https://www.sparkfun.com/products/13279

// Lane 1 (Right – facing end)
#define LANE_ONE_DISPLAY true
#define LANE_ONE_DISPLAY_CLK 12 // blue
#define LANE_ONE_DISPLAY_LAT 11 // green
#define LANE_ONE_DISPLAY_SER 13 // orange

// Lane 2 Center lane
#define LANE_TWO_DISPLAY true
#define LANE_TWO_DISPLAY_CLK 9 // blue
#define LANE_TWO_DISPLAY_LAT 8 // green
#define LANE_TWO_DISPLAY_SER 10 // orange

// Lane 3 (Left – facing end)
#define LANE_THREE_DISPLAY true
#define LANE_THREE_DISPLAY_CLK 6 // blue
#define LANE_THREE_DISPLAY_LAT 5 // green
#define LANE_THREE_DISPLAY_SER 7 // orange
/*
* Setup
*/
void setup() {
if(DEBUG) {
Serial.begin(9600, SERIAL_8N1); // pins 0,1 used for serial communication
}

// *** Initialize pin mode ***

/*
* It is possible (but not done here) that the release and reset buttons could have interrupt
* service routines of their own rather than reading the button in the main loop().
*
* However the release button has INT5, but the reset button would need to use a PC interrupt (PCINT10)
* since all the other external interrupts have been used on the Mega.
*
* I think this would make more sense to do if the button debounce was done in hardware.
*/

// initialize release button state
#if defined(RELEASE_BUTTON_PIN)
pinMode(RELEASE_BUTTON_PIN, INPUT);
lastStableReleaseButtonState = digitalRead(RELEASE_BUTTON_PIN);
#endif

// initialize reset button state
#if defined(RESET_BUTTON_PIN)
pinMode(RESET_BUTTON_PIN, INPUT);
lastStableResetButtonState = digitalRead(RESET_BUTTON_PIN);
#endif

#if defined(LANE_ONE_LIGHT_SENSE_PIN)
pinMode(LANE_ONE_LIGHT_SENSE_PIN, INPUT);
#endif

#if defined(LANE_TWO_LIGHT_SENSE_PIN)
pinMode(LANE_TWO_LIGHT_SENSE_PIN, INPUT);
#endif

#if defined(LANE_THREE_LIGHT_SENSE_PIN)
pinMode(LANE_THREE_LIGHT_SENSE_PIN, INPUT);
#endif

#if defined(LASER_PIN)
pinMode(LASER_PIN, OUTPUT);
digitalWrite(LASER_PIN, LOW);
#endif

#if defined(LIGHT_PIN)
pinMode(LIGHT_PIN, OUTPUT);
digitalWrite(LIGHT_PIN, LOW);
#endif

#if defined(LANE_ONE_DISPLAY)
pinMode(LANE_ONE_DISPLAY_CLK, OUTPUT);
pinMode(LANE_ONE_DISPLAY_LAT, OUTPUT);
pinMode(LANE_ONE_DISPLAY_SER, OUTPUT);

digitalWrite(LANE_ONE_DISPLAY_CLK, LOW);
digitalWrite(LANE_ONE_DISPLAY_LAT, LOW);
digitalWrite(LANE_ONE_DISPLAY_SER, LOW);
#endif

#if defined(LANE_TWO_DISPLAY)
pinMode(LANE_TWO_DISPLAY_CLK, OUTPUT);
pinMode(LANE_TWO_DISPLAY_LAT, OUTPUT);
pinMode(LANE_TWO_DISPLAY_SER, OUTPUT);

digitalWrite(LANE_TWO_DISPLAY_CLK, LOW);
digitalWrite(LANE_TWO_DISPLAY_LAT, LOW);
digitalWrite(LANE_TWO_DISPLAY_SER, LOW);
#endif

#if defined(LANE_THREE_DISPLAY)
pinMode(LANE_THREE_DISPLAY_CLK, OUTPUT);
pinMode(LANE_THREE_DISPLAY_LAT, OUTPUT);
pinMode(LANE_THREE_DISPLAY_SER, OUTPUT);

digitalWrite(LANE_THREE_DISPLAY_CLK, LOW);
digitalWrite(LANE_THREE_DISPLAY_LAT, LOW);
digitalWrite(LANE_THREE_DISPLAY_SER, LOW);
#endif

previousMillis = millis();

/*
* These displays use I2C to communicate, 2 pins are required to
* interface. There are multiple selectable I2C addresses. For backpacks
* with 2 Address Select pins: 0x70, 0x71, 0x72 or 0x73. For backpacks
* with 3 Address Select pins: 0x70 thru 0x77
*
* This circuit assumes the default address of 0x70 – no select pins need to be soldered.
*
* SDA – pin 20 white
* SCL – pin 21 blue
*/
matrix.begin(0x70);
matrix.setBrightness(15); // 0..15
matrix.clear();

diagnostics();
}
void loop() {

// Note: diagnostics may not have passed

// note that millis() briefly turns interrupts off! http://gammon.com.au/interrupts

/*
* Button debounce
* At least last MIN_STABLE_VALS reads should be consistent and different than last stable state.
* Release button state will be checked every RELEASE_BUTTON_CHECK_INTERVAL milliseconds.
*/
if((millis() – previousMillis) > BUTTON_CHECK_INTERVAL)
{
previousMillis += BUTTON_CHECK_INTERVAL;

#if defined(RELEASE_BUTTON_PIN)
// Check RELEASE button
if(digitalRead(RELEASE_BUTTON_PIN) != lastStableReleaseButtonState)
{
stableValsRelease++;
if(stableValsRelease >= MIN_STABLE_VALS)
{
if(DEBUG){
Serial.print(“Release button state changed: “);
Serial.println(!lastStableReleaseButtonState, BIN);
}

lastStableReleaseButtonState = !lastStableReleaseButtonState;
stableValsRelease = 0;

// trigger state change
if(lastStableReleaseButtonState == LOW) { // button down
releaseKars();
} /*
else { // button up
// do nothing
} */
}
}
else {
stableValsRelease = 0;
}
#endif

/*
* Need to read the same value for MIN_STABLE_VALS in a row in order to
* be recorded as a state change for the button
*/
#if defined(RESET_BUTTON_PIN)
// Check RESET button
if(digitalRead(RESET_BUTTON_PIN) != lastStableResetButtonState)
{
stableValsReset++;
if(stableValsReset >= MIN_STABLE_VALS)
{
if(DEBUG){
Serial.print(“Reset button state changed: “);
Serial.println(!lastStableResetButtonState, BIN);
}

lastStableResetButtonState = !lastStableResetButtonState;
stableValsReset = 0;

// trigger state change
if(lastStableResetButtonState == LOW) { // button down
resetKars();
} /*
else { // button up
// do nothing
} */
}
}
else {
stableValsReset = 0;
}
#endif
}

/*
* An ISR triggered immediately (within 10ms) so ignore it.
* This is a kludge to deal with events that get triggered as soon as
* an interrupt is registered.
* Ideally EIFR should clear these events!
* This seems to happen only on the first run.
*/
if((millis() – startTime) < 10) {
laneOnePosition = 0;
laneTwoPosition = 0;
laneThreePosition = 0;
}

/*
* Handle the case where the race is started, but nothing happens
* The light will stay on and potentially overheat, so automatically reset
* (turning off light) after LIGHT_TIMEOUT milliseconds.
*/

if((startTime > 0) && ((millis() – startTime) > LIGHT_TIMEOUT)) {
if(DEBUG){
Serial.println(“Timeout”);
}
// A reset will erase the winning time and lane, so only turn off light
enableLight(false);
}

/*
Race is started, but no finishes yet.
Show timer count
*/
if(startTime > 0 && winnerTime == 0) {
displayTimer((millis() – startTime));
}

/*
* Check for finishes
* Blink the winning time.
* 0 = No Blinking
* 1 = Blink at 2Hz
* 2 = Blink at 1Hz
* 3 = Blink at 1/2 Hz
*/

if(laneOnePosition > 0 && !laneOneFinished){
laneOneFinished = true;
if(DEBUG){
Serial.println(“Lane 1 finished”);
}

if(laneOnePosition == 1) { // winner
// stop timer
winnerTime = (millis() – startTime); // note that millis() briefly turns interrupts off

resetBigDigits(); // clear “go”

// display timer
displayTimer(winnerTime);
matrix.blinkRate(2);
}
// display ‘laneOnePosition’ over Lane 1
displayLane(1,false,laneOnePosition);
}

if(laneTwoPosition > 0 && !laneTwoFinished){
laneTwoFinished = true;
if(DEBUG){
Serial.println(“Lane 2 finished”);
}

if(laneTwoPosition == 1) { // winner
// stop timer
winnerTime = (millis() – startTime); // note that millis() briefly turns interrupts off

resetBigDigits(); // clear “go”

// display timer
displayTimer(winnerTime);
matrix.blinkRate(2);
}

// display ‘laneTwoPosition’ over Lane 2
displayLane(2,false,laneTwoPosition);
}

if(laneThreePosition > 0 && !laneThreeFinished){
laneThreeFinished = true;
if(DEBUG){
Serial.println(“Lane 3 finished”);
}

if(laneThreePosition == 1) { // winner
// stop timer
winnerTime = (millis() – startTime); // note that millis() briefly turns interrupts off

resetBigDigits(); // clear “go”

// display timer
displayTimer(winnerTime);
matrix.blinkRate(2);
}

// display ‘laneThreePosition’ over Lane 3
displayLane(3,false,laneThreePosition);
}
} // end loop()
/**
* Each lane will initially display 8 and timer will display 88:88
* The laser will be turned on.
*
* Wait.
*
* The light will be turned on.
* Each lane that detects the light will display a decimal point (.). If light is not detected, the lane will display ‘E.’ (with decimal point).
*
* For the next LIGHT_TIMEOUT seconds, each available lane that passed the light detection test will attempt to detect darkness.
* If successfully detected, the lane decimal point will disappear (so lane displays nothing).
*
* At the end of the diagnostic each available lane will display one of the following:
* ‘ ‘ (nothing) – passed light and dark detection
* ‘E.’ (E with decimal) – failed to detect light; dark detection skipped
* ‘E’ (E without decimal) – passed light detection, but failed to detect dark (requires that sensor be manually covered during test).
*
*/
void diagnostics() {

// ********* Begin display diagnostic *********

// Test Lane 1 display ‘8’
#if defined(LANE_ONE_DISPLAY)
displayLane(1,false,8);
#endif

// Test Lane 2 display ‘8’
#if defined(LANE_TWO_DISPLAY)
displayLane(2,false,8);
#endif

// Test Lane 3 display ‘8’
#if defined(LANE_THREE_DISPLAY)
displayLane(3,false,8);
#endif

// Test timer display ’88:88′
matrix.drawColon(true);
matrix.writeDigitNum(0,8,false);
matrix.writeDigitNum(1,8,false);
matrix.writeDigitNum(3,8,false);
matrix.writeDigitNum(4,8,false);
matrix.writeDisplay();
// displayTimer(8888); // will only display ‘8:88’ since last digit is dropped. Unable to pass in ‘88888’ since it exceeds unsigned int.

// turn laser on
#if defined(LASER_PIN)
digitalWrite(LASER_PIN, HIGH);
#endif

delay(3000);

resetDisplay();

// ********* Begin sensor diagnostic *********

enableLight(true);
startTime = millis(); // reuse startTime to track time since light turned on

bool laneOnePass = false;
bool laneTwoPass = false;
bool laneThreePass = false;

// All lanes should sense light (sensors should be low).

#if defined(LANE_ONE_LIGHT_SENSE_PIN)
if(senseLight(LANE_ONE_LIGHT_SENSE_PIN) == true) {
laneOnePass = true;
#if defined(LANE_ONE_DISPLAY)
displayLane(1,true,’ ‘);
#endif
if(DEBUG) {
Serial.println(“Lane 1 detect light pass”);
}
}
else {
// display “E.” for lane one
#if defined(LANE_ONE_DISPLAY)
displayLane(1,true,’E’);
#endif
if(DEBUG){
Serial.println(“Lane 1 failed to detect light”);
}
}
#endif

#if defined(LANE_TWO_LIGHT_SENSE_PIN)
if(senseLight(LANE_TWO_LIGHT_SENSE_PIN) == true) {
laneTwoPass = true;
#if defined(LANE_TWO_DISPLAY)
displayLane(2,true,’ ‘);
#endif
if(DEBUG) {
Serial.println(“Lane 2 detect light pass”);
}
}
else {
// display “E.” for lane two
#if defined(LANE_TWO_DISPLAY)
displayLane(2,true,’E’);
#endif
if(DEBUG){
Serial.println(“Lane 2 failed to detect light”);
}
}
#endif

#if defined(LANE_THREE_LIGHT_SENSE_PIN)
if(senseLight(LANE_THREE_LIGHT_SENSE_PIN) == true) {
laneThreePass = true;
#if defined(LANE_THREE_DISPLAY)
displayLane(3,true,’ ‘);
#endif
if(DEBUG) {
Serial.println(“Lane 3 detect light pass”);
}
}
else {
// display “E.” for lane two
#if defined(LANE_THREE_DISPLAY)
displayLane(3,true,’E’);
#endif
if(DEBUG){
Serial.println(“Lane 3 failed to detect light”);
}
}
#endif

// At this point lanes should display ‘E’ or the lane number.

// Now poll sensors looking for darkness
// User should cover each sensor until all correctly register darkness.

// Automatically pass lanes that failed to detect light to skip darkness detection test
laneOnePass = !laneOnePass;
laneTwoPass = !laneTwoPass;
laneThreePass = !laneThreePass;

// Ideally all three lane pass flags reset to false

// This diagnostic will run until all available light sensors register darkness, or the timeout is reached
while(!(laneOnePass && laneTwoPass && laneThreePass) && ((millis() – startTime) < LIGHT_TIMEOUT)) {

#if defined(LANE_ONE_LIGHT_SENSE_PIN)
if(senseLight(LANE_ONE_LIGHT_SENSE_PIN) == false) {
laneOnePass = true;
#if defined(LANE_ONE_DISPLAY)
displayLane(1,false,’ ‘); // make decimal point disappear
#endif
if(DEBUG) {
Serial.println(“Lane 1 detect dark pass”);
}
}
#endif

#if defined(LANE_TWO_LIGHT_SENSE_PIN)
if(senseLight(LANE_TWO_LIGHT_SENSE_PIN) == false) {
laneTwoPass = true;
#if defined(LANE_TWO_DISPLAY)
displayLane(2,false,’ ‘); // make decimal point disappear
#endif
if(DEBUG) {
Serial.println(“Lane 2 detect dark pass”);
}
}
#endif

#if defined(LANE_THREE_LIGHT_SENSE_PIN)
if(senseLight(LANE_THREE_LIGHT_SENSE_PIN) == false) {
laneThreePass = true;
#if defined(LANE_THREE_DISPLAY)
displayLane(3,false,’ ‘); // make decimal point disappear
#endif
if(DEBUG) {
Serial.println(“Lane 3 detect dark pass”);
}
}
#endif

} // end darkness detect loop

startTime = 0;

// turn light off
enableLight(false);

// Since darkness detect loop may have ended due to timeout,
// display any available lanes that did not pass

// Which lanes detected light, but not dark?
// Those lanes will have their SENSE_PIN defined, but the pass flag will be false.

#if defined(LANE_ONE_LIGHT_SENSE_PIN)
if(laneOnePass == false){
#if defined(LANE_ONE_DISPLAY)
// display “E” for lane one (no decimal point)
displayLane(1,false,’E’);
#endif
if(DEBUG){
Serial.println(“Lane 1 failed to detect darkness”);
}
}
#endif

#if defined(LANE_TWO_LIGHT_SENSE_PIN)
if(laneTwoPass == false){
#if defined(LANE_TWO_DISPLAY)
// display “E” for lane two (no decimal point)
displayLane(2,false,’E’);
#endif
if(DEBUG){
Serial.println(“Lane 2 failed to detect darkness”);
}
}
#endif

#if defined(LANE_THREE_LIGHT_SENSE_PIN)
if(laneThreePass == false){
#if defined(LANE_THREE_DISPLAY)
// display “E” for lane three (no decimal point)
displayLane(3,false,’E’);
#endif
if(DEBUG){
Serial.println(“Lane 3 failed to detect darkness”);
}
}
#endif

// leave laser on

if(DEBUG)
Serial.println(“Diagnostic finished”);
}

/*
* Detect presence of laser light in specified lane.
* Return true if light or false if dark.
* Used by diagnostics (not ISRs)
*
* Should read >3V if dark or <3V if light
*
* https://www.radioshack.com/products/radioshack-3mm-ambient-light-sensor-5-pack?variant=20331279749
* http://www.everlight.com/file/ProductFile/ALS-PDT144-6C-L451.pdf
*/
boolean senseLight(int pin) {

return (digitalRead(pin) == LOW); // a voltage less than 3 volts is present at the pin (5V boards)

}

/*
* Triggered when lane one transitions from LOW/light to HIGH/dark (RISING).
* No serial I/O, no millis(), no delay()
*/
void watchLaneOneISR() {

// interrupts(); // Normally interrupts are turned off when executing an ISR
if(laneTwoPosition == 1 || laneThreePosition == 1) { // first place already claimed
if(laneTwoPosition == 2 || laneThreePosition == 2) { // second place already claimed
laneOnePosition = 3;
}
else {
laneOnePosition = 2;
}
}
else {
laneOnePosition = 1;
}
}

/*
* Triggered when lane two transitions from LOW/light to HIGH/dark (RISING)
* No serial I/O, no millis(), no delay()
*/
void watchLaneTwoISR() {

// interrupts(); // Normally interrupts are turned off when executing an ISR
if(laneOnePosition == 1 || laneThreePosition == 1) { // first place already claimed
if(laneOnePosition == 2 || laneThreePosition == 2) { // second place already claimed
laneTwoPosition = 3;
}
else {
laneTwoPosition = 2;
}
}
else {
laneTwoPosition = 1;
}
}

/*
* Triggered when lane three transitions from LOW/light to HIGH/dark (RISING)
* No serial I/O, no millis(), no delay()
*/
void watchLaneThreeISR() {

// interrupts(); // Normally interrupts are turned off when executing an ISR
if(laneTwoPosition == 1 || laneOnePosition == 1) { // first place already claimed
if(laneTwoPosition == 2 || laneOnePosition == 2) { // second place already claimed
laneThreePosition = 3;
}
else {
laneThreePosition = 2;
}
}
else {
laneThreePosition = 1;
}
}

/*
* Release switch has just transitioned.
* The barrier is down and all Kub Kars are released.
* Triggered when lane light detection transitions from LOW/light to HIGH/dark (RISING).
* When interrupt is attached, light will be on so sensor will be LOW/light
*
* http://gammon.com.au/interrupts
*/
void releaseKars() {

if(startTime==0){ // if race not already in progress..

displayGo(); // will delay: 3,2,1, go!

// start the timer – uptime clock has been running since power on, simply record the uptime (in ms) at start
startTime = millis(); // 50 days before overflows to zero!

noInterrupts(); // disable interrupts

// turn light on
enableLight(true);

/*
// disable interrupts
EIMSK &= ~(1 << INT2);
EIMSK &= ~(1 << INT3);
EIMSK &= ~(1 << INT4);
*/
/*
// clear queued interrupts
EIFR = (1 << INTF2); // pin 19 – lane 1
EIFR = (1 << INTF3); // pin 18 – lane 2
EIFR = (1 << INTF4); // pin 2 – lane 3
*/

// clear queued interrupts on INT2, INT3, INT4
// EIFR – External Interrupt Flag Register
// If not cleared, then an interrupt is triggered as soon as attachInterrupt is called (regardless of trigger method – RISING, FALLING, etc)
// https://github.com/arduino/Arduino/issues/510
// https://forum.arduino.cc/index.php?topic=59217.15
/*
EIFR = bit (INTF2); // clear flag for interrupt 2 – pin 19 – lane 1
EIFR = bit (INTF3); // clear flag for interrupt 3 – pin 18 – lane 2
EIFR = bit (INTF4); // clear flag for interrupt 4 – pin 2 – lane 3
*/

#if defined(LANE_ONE_LIGHT_SENSE_PIN)
EIFR = bit (digitalPinToInterrupt(LANE_ONE_LIGHT_SENSE_PIN)); // clear queue interrupts
attachInterrupt(digitalPinToInterrupt(LANE_ONE_LIGHT_SENSE_PIN), watchLaneOneISR, RISING);
#endif

#if defined(LANE_TWO_LIGHT_SENSE_PIN)
EIFR = bit (digitalPinToInterrupt(LANE_TWO_LIGHT_SENSE_PIN)); // clear queue interrupts
attachInterrupt(digitalPinToInterrupt(LANE_TWO_LIGHT_SENSE_PIN), watchLaneTwoISR, RISING);
#endif

#if defined(LANE_THREE_LIGHT_SENSE_PIN)
EIFR = bit (digitalPinToInterrupt(LANE_THREE_LIGHT_SENSE_PIN)); // clear queue interrupts
attachInterrupt(digitalPinToInterrupt(LANE_THREE_LIGHT_SENSE_PIN), watchLaneThreeISR, RISING);
#endif

interrupts(); // enable interrupts

if(DEBUG)
Serial.println(“Release triggered”);
}
}

/*
* The Release switch has just transitioned.
* The barrier is up and all Kub Kars are held back.
*/
void resetKars() {

if(DEBUG)
Serial.println(“Reset triggered”);

#if defined(LANE_ONE_LIGHT_SENSE_PIN)
detachInterrupt(digitalPinToInterrupt(LANE_ONE_LIGHT_SENSE_PIN));
#endif

#if defined(LANE_TWO_LIGHT_SENSE_PIN)
detachInterrupt(digitalPinToInterrupt(LANE_TWO_LIGHT_SENSE_PIN));
#endif

#if defined(LANE_THREE_LIGHT_SENSE_PIN)
detachInterrupt(digitalPinToInterrupt(LANE_THREE_LIGHT_SENSE_PIN));
#endif

laneOnePosition = 0;
laneTwoPosition = 0;
laneThreePosition = 0;

laneOneFinished = false;
laneTwoFinished = false;
laneThreeFinished = false;

startTime = 0;
winnerTime = 0;

// turn light off; it may already be off
enableLight(false);

resetDisplay();
}

/*
* Turn light on or off
*/
void enableLight(bool enable) {

#if defined(LIGHT_PIN)
digitalWrite(LIGHT_PIN, (enable ? HIGH : LOW));
#endif
}

/*
* t – an integer from 0…9999
*
* https://www.adafruit.com/products/1264
* https://learn.adafruit.com/adafruit-led-backpack/1-2-inch-7-segment-backpack
*/
void displayTimer(unsigned int t) {

/*
* want 1234 (1.234 seconds) to be represented as 1:23
*/
t = t / 10; // drop least significant digit

if(DEBUG) {
Serial.print(“Timer: “); Serial.println(t, DEC);
}

unsigned int p = 4;
while(t > 0 && p >= 0) {
matrix.writeDigitNum(p,(t%10),false);
t=t/10;
p=p-1;
if(p==2) { p = 1; }
}

/*
* 0 = No Blinking
* 1 = Blink at 2Hz
* 2 = Blink at 1Hz
* 3 = Blink at 1/2 Hz
*/
matrix.blinkRate(0);
matrix.drawColon(true);
matrix.writeDisplay();
}
/*
* Clear the timer
*
* Clear each lane placement number
*/
void resetDisplay() {
matrix.clear(); //matrix.println(0);
// matrix.drawColon(false);
matrix.writeDisplay();
resetBigDigits();
}

/*
3,2,1, go
*/
void displayGo() {
matrix.clear();
displayLane(2,false,3);
delay(750);
displayLane(2,false,2);
delay(750);
displayLane(2,false,1);
delay(750);
displayLane(3,false,’g’);
displayLane(2,false,’o’);
displayLane(1,false,’!’);
}
void resetBigDigits() {
#if defined(LANE_ONE_DISPLAY)
displayLane(1,false,’ ‘);
#endif

#if defined(LANE_TWO_DISPLAY)
displayLane(2,false,’ ‘);
#endif

#if defined(LANE_THREE_DISPLAY)
displayLane(3,false,’ ‘);
#endif
}

/*
* Given a number, or ‘-‘, shifts it out to the display
*
* number is 0..9, [space], c, -, E, h, m, l
* decimal is true|false
* laneNumber is 1,2,3 – otherwise defaults to 1
*/
void displayLane(byte laneNumber, boolean decimal, byte number)
{
byte segmentClock; // clock pin
byte segmentLatch; // latch pin
byte segmentData; // serial data pin

switch(laneNumber) {
case 1:
segmentClock = LANE_ONE_DISPLAY_CLK;
segmentLatch = LANE_ONE_DISPLAY_LAT;
segmentData = LANE_ONE_DISPLAY_SER;
break;
case 2:
segmentClock = LANE_TWO_DISPLAY_CLK;
segmentLatch = LANE_TWO_DISPLAY_LAT;
segmentData = LANE_TWO_DISPLAY_SER;
break;
case 3:
segmentClock = LANE_THREE_DISPLAY_CLK;
segmentLatch = LANE_THREE_DISPLAY_LAT;
segmentData = LANE_THREE_DISPLAY_SER;
break;
default:
segmentClock = LANE_TWO_DISPLAY_CLK;
segmentLatch = LANE_TWO_DISPLAY_LAT;
segmentData = LANE_TWO_DISPLAY_SER;
break;
}

// – A
// / / F/B
// – G
// / / E/C
// -. D/DP

#define a 1<<0
#define b 1<<6
#define c 1<<5
#define d 1<<4
#define e 1<<3
#define f 1<<1
#define g 1<<2
#define dp 1<<7

byte segments;

switch (number)
{
case 1: segments = b | c; break;
case 2: segments = a | b | d | e | g; break;
case 3: segments = a | b | c | d | g; break;
case 4: segments = f | g | b | c; break;
case 5: segments = a | f | g | c | d; break;
case 6: segments = a | f | g | e | c | d; break;
case 7: segments = a | b | c; break;
case 8: segments = a | b | c | d | e | f | g; break;
case 9: segments = a | b | c | d | f | g; break;
case 0: segments = a | b | c | d | e | f; break;
case ‘ ‘: segments = 0; break;
case ‘c’: segments = g | e | d; break;
case ‘-‘: segments = g; break;
case ‘E’: segments = a | f | g | e | d; break;
case ‘h’: segments = a | g | d; break; // three horizontal bars
case ‘m’: segments = g | d; break; // two horizontal bars
case ‘l’: segments = d; break; // one horizontal bar
case ‘S’: segments = a | f | g | c | d; break;
case ‘n’: segments = g | e | c; break;
case ‘d’: segments = b | g | e | c | d; break;
case ‘g’: segments = a | f | b | g | c | d; break;
case ‘o’: segments = a | f | b | g; break;
case ‘!’: segments = b | dp; break; // do not enable decimal
}

if (decimal) segments |= dp;

//Clock these bits out to the drivers
for (byte x = 0 ; x < 8 ; x++)
{
digitalWrite(segmentClock, LOW);
digitalWrite(segmentData, segments & 1 << (7 – x));
digitalWrite(segmentClock, HIGH); //Data transfers to the register on the rising edge of SRCK
}

//Latch the current segment data
digitalWrite(segmentLatch, LOW);
digitalWrite(segmentLatch, HIGH); //Register moves storage register on the rising edge of RCK
}

Resources

MOSFETs – http://bildr.org/2012/03/rfp30n06le-arduino/

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s