Building a Pinewood Derby Timer: Finishing the arch and adding the last of the wiring!

Well, I finally finished it! The pinewood derby timer is done. Check it out!

First, I had to finish the wiring for the lights and the sensors, and then I had to finish the arch.

Then, I had to program it. Fortunately, https://www.dfgtec.com/ has a whole section on programming the board, and what settings you should change or adjust for your needs. Super helpful site!

Here is my final code:

/*================================================================================*
Pinewood Derby Timer Version 3.00 – 31 Oct 2016
http://www.dfgtec.com/pdt

Flexible and affordable Pinewood Derby timer that interfaces with the
following software:
– PD Test/Tune/Track Utility
– Grand Prix Race Manager software

Refer to the website for setup and usage instructions.
Copyright (C) 2011-2018 David Gadberry

This work is licensed under the Creative Commons Attribution-NonCommercial-
ShareAlike 3.0 Unported License. To view a copy of this license, visit
http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter to
Creative Commons, 444 Castro Street, Suite 900, Mountain View, California,
94041, USA.
*================================================================================*/

/*—————————————–*
– TIMER CONFIGURATION –
*—————————————–*/
#define NUM_LANES 4 // number of lanes

#define LED_DISPLAY 1 // Enable lane place/time displays
//#define DUAL_DISP 1 // dual displays (front/back) per lane (4 lanes max)
//#define LARGE_DISP 1 // utilize large Adafruit displays (see website)
#define SHOW_PLACE 1 // Show place mode
#define PLACE_DELAY 4 // Delay (secs) when displaying place/time
#define MIN_BRIGHT 4 // minimum display brightness (0-15)
#define MAX_BRIGHT 10 // maximum display brightness (0-15)

#define GATE_RESET 1 // Enable closing start gate to reset timer
/*—————————————–*
– END –
*—————————————–*/
#ifdef LED_DISPLAY // LED control libraries
#include “Wire.h”
#include “Adafruit_LEDBackpack.h”
#include “Adafruit_GFX.h”
#endif

/*—————————————–*
– static definitions –
*—————————————–*/
#define PDT_VERSION “3.00” // software version
#define MAX_LANE 6 // maximum number of lanes (Uno)
#define MAX_DISP 8 // maximum number of displays (Adafruit)

#define mREADY 0 // program modes
#define mRACING 1
#define mFINISH 2
#define mTEST 3

#define START_TRIP LOW // start switch trip condition
#define NULL_TIME 99.999 // null (non-finish) time
#define NUM_DIGIT 4 // timer resolution (# of decimals)
#define DISP_DIGIT 4 // total number of display digits

#define PWM_LED_ON 220
#define PWM_LED_OFF 255
#define char2int(c) (c – ‘0’)

//
// serial messages <- to timer
// -> from timer
//
#define SMSG_ACKNW ‘.’ // -> acknowledge message

#define SMSG_POWER ‘P’ // -> start-up (power on or hard reset)

#define SMSG_CGATE ‘G’ // <- check gate
#define SMSG_GOPEN ‘O’ // -> gate open

#define SMSG_RESET ‘R’ // <- reset
#define SMSG_READY ‘K’ // -> ready

#define SMSG_SOLEN ‘S’ // <- start solenoid
#define SMSG_START ‘B’ // -> race started
#define SMSG_FORCE ‘F’ // <- force end
#define SMSG_RSEND ‘Q’ // <- resend race data

#define SMSG_LMASK ‘M’ // <- mask lane
#define SMSG_UMASK ‘U’ // <- unmask all lanes

#define SMSG_GVERS ‘V’ // <- request timer version
#define SMSG_DEBUG ‘D’ // <- toggle debug on/off
#define SMSG_GNUML ‘N’ // <- request number of lanes

/*—————————————–*
– pin assignments –
*—————————————–*/
byte BRIGHT_LEV = A0; // brightness level
byte RESET_SWITCH = 8; // reset switch
byte STATUS_LED_R = 9; // status LED (red)
byte STATUS_LED_B = 11; // status LED (blue)
byte STATUS_LED_G = 10; // status LED (green)
byte START_GATE = 12; // start gate switch
byte START_SOL = 13; // start solenoid

// Display # 1 2 3 4 5 6 7 8
int DISP_ADD [MAX_DISP] = {0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77}; // display I2C addresses

// Lane # 1 2 3 4 5 6
byte LANE_DET [MAX_LANE] = { 2, 3, 4, 5, 6, 7}; // finish detection pins

/*—————————————–*
– global variables –
*—————————————–*/
boolean fDebug = false; // debug flag
boolean ready_first; // first pass in ready state flag
boolean finish_first; // first pass in finish state flag

unsigned long start_time; // race start time (microseconds)
unsigned long lane_time [MAX_LANE]; // lane finish time (microseconds)
int lane_place [MAX_LANE]; // lane finish place
boolean lane_mask [MAX_LANE]; // lane mask status

int serial_data; // serial data
byte mode; // current program mode

float display_level = -1.0; // display brightness level

#ifdef LARGE_DISP
unsigned char msgGateC[] = {0x6D, 0x41, 0x00, 0x0F, 0x07}; // S=CL
unsigned char msgGateO[] = {0x6D, 0x41, 0x00, 0x3F, 0x5E}; // S=OP
unsigned char msgLight[] = {0x41, 0x41, 0x00, 0x00, 0x07}; // == L
unsigned char msgDark [] = {0x41, 0x41, 0x00, 0x00, 0x73}; // == d
#else
unsigned char msgGateC[] = {0x6D, 0x48, 0x00, 0x39, 0x38}; // S=CL
unsigned char msgGateO[] = {0x6D, 0x48, 0x00, 0x3F, 0x73}; // S=OP
unsigned char msgLight[] = {0x48, 0x48, 0x00, 0x00, 0x38}; // == L
unsigned char msgDark [] = {0x48, 0x48, 0x00, 0x00, 0x5e}; // == d
#endif
unsigned char msgDashT[] = {0x40, 0x40, 0x00, 0x40, 0x40}; // —-
unsigned char msgDashL[] = {0x00, 0x00, 0x00, 0x40, 0x00}; // –
unsigned char msgBlank[] = {0x00, 0x00, 0x00, 0x00, 0x00}; // (blank)

#ifdef LED_DISPLAY // LED display control
Adafruit_7segment disp_mat[MAX_DISP];
#endif

void initialize(boolean powerup=false);
void dbg(int, const char * msg, int val=-999);
void smsg(char msg, boolean crlf=true);
void smsg_str(const char * msg, boolean crlf=true);

/*================================================================================*
SETUP TIMER
*================================================================================*/
void setup()
{
/*—————————————–*
– hardware setup –
*—————————————–*/
pinMode(STATUS_LED_R, OUTPUT);
pinMode(STATUS_LED_B, OUTPUT);
pinMode(STATUS_LED_G, OUTPUT);
pinMode(START_SOL, OUTPUT);
pinMode(RESET_SWITCH, INPUT);
pinMode(START_GATE, INPUT);
pinMode(BRIGHT_LEV, INPUT);

digitalWrite(RESET_SWITCH, HIGH); // enable pull-up resistor
digitalWrite(START_GATE, HIGH); // enable pull-up resistor

digitalWrite(START_SOL, LOW);

#ifdef LED_DISPLAY
for (int n=0; n<MAX_DISP; n++)
{
disp_mat[n] = Adafruit_7segment();
disp_mat[n].begin(DISP_ADD[n]);
disp_mat[n].clear();
disp_mat[n].drawColon(false);
disp_mat[n].writeDisplay();
}
#endif

for (int n=0; n<MAX_LANE; n++)
{
pinMode(LANE_DET[n], INPUT);

digitalWrite(LANE_DET[n], HIGH); // enable pull-up resistor
}
set_display_brightness();

/*—————————————–*
– software setup –
*—————————————–*/
Serial.begin(9600);
smsg(SMSG_POWER);

/*—————————————–*
– check for test mode –
*—————————————–*/
if (digitalRead(RESET_SWITCH) == LOW)
{
mode = mTEST;
test_pdt_hw();
}

/*—————————————–*
– initialize timer –
*—————————————–*/
initialize(true);
unmask_all_lanes();
}
/*================================================================================*
MAIN LOOP
*================================================================================*/
void loop()
{
process_general_msgs();

switch (mode)
{
case mREADY:
timer_ready_state();
break;
case mRACING:
timer_racing_state();
break;
case mFINISH:
timer_finished_state();
break;
}
}
/*================================================================================*
TIMER READY STATE
*================================================================================*/
void timer_ready_state()
{
if (ready_first)
{
set_status_led();
clear_displays();

ready_first = false;
}

if (serial_data == int(SMSG_SOLEN)) // activate start solenoid
{
digitalWrite(START_SOL, HIGH);
smsg(SMSG_ACKNW);
}

if (digitalRead(START_GATE) == START_TRIP) // timer start
{
start_time = micros();

digitalWrite(START_SOL, LOW);

smsg(SMSG_START);
delay(100);

mode = mRACING;
}

return;
}

/*================================================================================*
TIMER RACING STATE
*================================================================================*/
void timer_racing_state()
{
int lanes_left, finish_order, lane_status[NUM_LANES];
unsigned long current_time, last_finish_time;
set_status_led();
clear_displays();

finish_order = 0;
last_finish_time = 0;

lanes_left = NUM_LANES;
for (int n=0; n<NUM_LANES; n++)
{
if (lane_mask[n]) lanes_left–;
}

while (lanes_left)
{
current_time = micros();

for (int n=0; n<NUM_LANES; n++) lane_status[n] = bitRead(PIND, LANE_DET[n]); // read status of all lanes

for (int n=0; n<NUM_LANES; n++)
{
if (lane_time[n] == 0 && lane_status[n] == HIGH && !lane_mask[n]) // car has crossed finish line
{
lanes_left–;

lane_time[n] = current_time – start_time;

if (lane_time[n] > last_finish_time)
{
finish_order++;
last_finish_time = lane_time[n];
}
lane_place[n] = finish_order;

update_display(n, lane_place[n], lane_time[n], SHOW_PLACE);
}
}

serial_data = get_serial_data();

if (serial_data == int(SMSG_FORCE) || serial_data == int(SMSG_RESET) || digitalRead(RESET_SWITCH) == LOW) // force race to end
{
lanes_left = 0;
smsg(SMSG_ACKNW);
}
}

send_race_results();

mode = mFINISH;

return;
}
/*================================================================================*
TIMER FINISHED STATE
*================================================================================*/
void timer_finished_state()
{
if (finish_first)
{
set_status_led();
finish_first = false;
}

if (GATE_RESET && digitalRead(START_GATE) != START_TRIP) // gate closed
{
delay(500); // ignore any switch bounce

if (digitalRead(START_GATE) != START_TRIP) // gate still closed
{
initialize(); // reset timer
}
}

if (serial_data == int(SMSG_RSEND)) // resend race data
{
smsg(SMSG_ACKNW);
send_race_results();
}

set_display_brightness();
display_race_results();

return;
}
/*================================================================================*
PROCESS GENERAL SERIAL MESSAGES
*================================================================================*/
void process_general_msgs()
{
int lane;
char tmps[50];
serial_data = get_serial_data();

if (serial_data == int(SMSG_GVERS)) // get software version
{
sprintf(tmps, “vert=%s”, PDT_VERSION);
smsg_str(tmps);
}

else if (serial_data == int(SMSG_GNUML)) // get number of lanes
{
sprintf(tmps, “numl=%d”, NUM_LANES);
smsg_str(tmps);
}

else if (serial_data == int(SMSG_DEBUG)) // toggle debug
{
fDebug = !fDebug;
dbg(true, “toggle debug = “, fDebug);
}

else if (serial_data == int(SMSG_CGATE)) // check start gate
{
if (digitalRead(START_GATE) == START_TRIP) // gate open
{
smsg(SMSG_GOPEN);
}
else
{
smsg(SMSG_ACKNW);
}
}

else if (serial_data == int(SMSG_RESET) || digitalRead(RESET_SWITCH) == LOW) // timer reset
{
if (digitalRead(START_GATE) != START_TRIP) // only reset if gate closed
{
initialize();
}
else
{
smsg(SMSG_GOPEN);
}
}

else if (serial_data == int(SMSG_LMASK)) // lane mask
{
delay(100);
serial_data = get_serial_data();

lane = serial_data – 48;
if (lane >= 1 && lane <= NUM_LANES)
{
lane_mask[lane-1] = true;

dbg(fDebug, “set mask on lane = “, lane);
}
smsg(SMSG_ACKNW);
}

else if (serial_data == int(SMSG_UMASK)) // unmask all lanes
{
unmask_all_lanes();
smsg(SMSG_ACKNW);
}

return;
}
/*================================================================================*
TEST PDT FINISH DETECTION HARDWARE
*================================================================================*/
void test_pdt_hw()
{
int lane_status[NUM_LANES];
char ctmp[10];
smsg_str(“TEST MODE”);
set_status_led();
delay(2000);

/*—————————————–*
show status of lane detectors
*—————————————–*/
while(true)
{
for (int n=0; n<NUM_LANES; n++)
{
lane_status[n] = bitRead(PIND, LANE_DET[n]); // read status of all lanes

if (lane_status[n] == HIGH)
{
update_display(n, msgDark); smsg_str(“Dark”); smsg_str(n);
}
else
{
update_display(n, msgLight); smsg_str(“Light”); smsg_str(n);
}
}

if (digitalRead(RESET_SWITCH) == LOW) // break out of this test
{
clear_displays();
delay(1000);
break;
}
delay(100);
}

/*—————————————–*
show status of start gate switch
*—————————————–*/
while(true)
{
if (digitalRead(START_GATE) == START_TRIP)
{
update_display(0, msgGateO);
}
else
{
update_display(0, msgGateC);
}

if (digitalRead(RESET_SWITCH) == LOW) // break out of this test
{
clear_displays();
delay(1000);
break;
}
delay(100);
}

/*—————————————–*
show pattern for brightness adjustment
*—————————————–*/
while(true)
{
#ifdef LED_DISPLAY
set_display_brightness();

for (int n=0; n<NUM_LANES; n++)
{
sprintf(ctmp,”%04d”, (int)display_level);

disp_mat[n].clear();

disp_mat[n].writeDigitNum(0, char2int(ctmp[0]), false);
disp_mat[n].writeDigitNum(1, char2int(ctmp[1]), false);
disp_mat[n].writeDigitNum(3, char2int(ctmp[2]), false);
disp_mat[n].writeDigitNum(4, char2int(ctmp[3]), false);

disp_mat[n].drawColon(false);
disp_mat[n].writeDisplay();

#ifdef DUAL_DISP
disp_mat[n+4].clear();

disp_mat[n+4].writeDigitNum(0, char2int(ctmp[0]), false);
disp_mat[n+4].writeDigitNum(1, char2int(ctmp[1]), false);
disp_mat[n+4].writeDigitNum(3, char2int(ctmp[2]), false);
disp_mat[n+4].writeDigitNum(4, char2int(ctmp[3]), false);

disp_mat[n+4].drawColon(false);
disp_mat[n+4].writeDisplay();
#endif
}
#endif

if (digitalRead(RESET_SWITCH) == LOW) // break out of this test
{
clear_displays();
break;
}
delay(1000);
}
}
/*================================================================================*
SEND RACE RESULTS TO COMPUTER
*================================================================================*/
void send_race_results()
{
float lane_time_sec;
for (int n=0; n<NUM_LANES; n++) // send times to computer
{
lane_time_sec = (float)(lane_time[n] / 1000000.0); // elapsed time (seconds)

if (lane_time_sec == 0) // did not finish
{
lane_time_sec = NULL_TIME;
}

Serial.print(n+1);
Serial.print(” – “);
Serial.println(lane_time_sec, NUM_DIGIT); // numbers are rounded to NUM_DIGIT
// digits by println function
}

return;
}
/*================================================================================*
RACE FINISHED – DISPLAY PLACE / TIME FOR ALL LANES
*================================================================================*/
void display_race_results()
{
unsigned long now;
static boolean display_mode;
static unsigned long last_display_update = 0;
if (!SHOW_PLACE) return;

now = millis();

if (last_display_update == 0) // first cycle
{
last_display_update = now;
display_mode = false;
}

if ((now – last_display_update) > (unsigned long)(PLACE_DELAY * 1000))
{
dbg(fDebug, “display_race_results”);

for (int n=0; n<NUM_LANES; n++)
{
update_display(n, lane_place[n], lane_time[n], display_mode);
}

display_mode = !display_mode;
last_display_update = now;
}

return;
}
/*================================================================================*
SEND MESSAGE TO DISPLAY
*================================================================================*/
void update_display(int lane, unsigned char msg[])
{

#ifdef LED_DISPLAY
disp_mat[lane].clear();
#ifdef DUAL_DISP
disp_mat[lane+4].clear();
#endif

for (int d = 0; d<=4; d++)
{
disp_mat[lane].writeDigitRaw(d, msg[d]);
#ifdef DUAL_DISP
disp_mat[lane+4].writeDigitRaw(d, msg[d]);
#endif
}

disp_mat[lane].writeDisplay();
#ifdef DUAL_DISP
disp_mat[lane+4].writeDisplay();
#endif
#endif

return;
}
/*================================================================================*
UPDATE LANE PLACE/TIME DISPLAY
*================================================================================*/
void update_display(int lane, int display_place, unsigned long display_time, int display_mode)
{
int c;
char ctime[10], cplace[4];
double display_time_sec;
boolean showdot;

// dbg(fDebug, “led: lane = “, lane);
// dbg(fDebug, “led: plce = “, display_place);
// dbg(fDebug, “led: time = “, display_time);

#ifdef LED_DISPLAY
if (display_mode)
{
if (display_place > 0)
{
disp_mat[lane].clear();
disp_mat[lane].drawColon(false);

sprintf(cplace,”%1d”, display_place);
disp_mat[lane].writeDigitNum(3, char2int(cplace[0]), false);

disp_mat[lane].writeDisplay();

#ifdef DUAL_DISP
disp_mat[lane+4].clear();
disp_mat[lane+4].drawColon(false);
disp_mat[lane+4].writeDigitNum(3, char2int(cplace[0]), false);
disp_mat[lane+4].writeDisplay();
#endif
}
else // did not finish
{
update_display(lane, msgDashL);
}
}
else
{
if (display_time > 0)
{
disp_mat[lane].clear();
disp_mat[lane].drawColon(false);
#ifdef DUAL_DISP
disp_mat[lane+4].clear();
disp_mat[lane+4].drawColon(false);
#endif

display_time_sec = (double)(display_time / (double)1000000.0); // elapsed time (seconds)
dtostrf(display_time_sec, (DISP_DIGIT+1), DISP_DIGIT, ctime); // convert to string

// Serial.print(“ctime = [“); Serial.print(ctime); Serial.println(“]”);
c = 0;
for (int d = 0; d<DISP_DIGIT; d++)
{
#ifdef LARGE_DISP
showdot = false;
#else
showdot = (ctime[c + 1] == ‘.’);
#endif
disp_mat[lane].writeDigitNum(d + int(d / 2), char2int(ctime[c]), showdot); // time
#ifdef DUAL_DISP
disp_mat[lane+4].writeDigitNum(d + int(d / 2), char2int(ctime[c]), showdot); // time
#endif

c++; if (ctime[c] == ‘.’) c++;
}

#ifdef LARGE_DISP
disp_mat[lane].writeDigitRaw(2, 16);
#ifdef DUAL_DISP
disp_mat[lane+4].writeDigitRaw(2, 16);
#endif
#endif

disp_mat[lane].writeDisplay();
#ifdef DUAL_DISP
disp_mat[lane+4].writeDisplay();
#endif
}
else // did not finish
{
update_display(lane, msgDashT);
}
}
#endif

return;
}
/*================================================================================*
CLEAR LANE PLACE/TIME DISPLAYS
*================================================================================*/
void clear_displays()
{
dbg(fDebug, “led: CLEAR”);

for (int n=0; n<NUM_LANES; n++)
{
if (mode == mRACING || mode == mTEST)
{
update_display(n, msgBlank); // racing
}
else
{
update_display(n, msgDashT); // ready
}
}

return;
}
/*================================================================================*
SET LANE DISPLAY BRIGHTNESS
*================================================================================*/
void set_display_brightness()
{
float new_level;

#ifdef LED_DISPLAY
new_level = long(1023 – analogRead(BRIGHT_LEV)) / 1023.0F * 15.0F;
new_level = min(new_level, (float)MAX_BRIGHT);
new_level = max(new_level, (float)MIN_BRIGHT);

if (fabs(new_level – display_level) > 0.3F) // deadband to prevent flickering
{ // between levels
dbg(fDebug, “led: BRIGHT”);

display_level = new_level;

for (int n=0; n<NUM_LANES; n++)
{
disp_mat[n].setBrightness((int)display_level);
#ifdef DUAL_DISP
disp_mat[n+4].setBrightness((int)display_level);
#endif
}
}
#endif

return;
}
/*================================================================================*
SET TIMER STATUS LED
*================================================================================*/
void set_status_led()
{
int r_lev, b_lev, g_lev;

dbg(fDebug, “status led = “, mode);

r_lev = PWM_LED_OFF;
b_lev = PWM_LED_OFF;
g_lev = PWM_LED_OFF;

if (mode == mREADY) // blue
{
b_lev = PWM_LED_ON;
}
else if (mode == mRACING) // green
{
g_lev = PWM_LED_ON;
}
else if (mode == mFINISH) // red
{
r_lev = PWM_LED_ON;
}
else if (mode == mTEST) // yellow
{
r_lev = PWM_LED_ON;
g_lev = PWM_LED_ON;
}

analogWrite(STATUS_LED_R, r_lev);
analogWrite(STATUS_LED_B, b_lev);
analogWrite(STATUS_LED_G, g_lev);

return;
}
/*================================================================================*
READ SERIAL DATA FROM COMPUTER
*================================================================================*/
int get_serial_data()
{
int data = 0;

if (Serial.available() > 0)
{
data = Serial.read();
dbg(fDebug, “ser rec = “, data);
}

return data;
}
/*================================================================================*
INITIALIZE TIMER
*================================================================================*/
void initialize(boolean powerup)
{
for (int n=0; n<NUM_LANES; n++)
{
lane_time[n] = 0;
lane_place[n] = 0;
}

start_time = 0;
set_status_led();
digitalWrite(START_SOL, LOW);

// if power up and gate is open -> goto FINISH state
if (powerup && digitalRead(START_GATE) == START_TRIP)
{
mode = mFINISH;
}
else
{
mode = mREADY;

smsg(SMSG_READY);
delay(100);
}
Serial.flush();

ready_first = true;
finish_first = true;

return;
}
/*================================================================================*
UNMASK ALL LANES
*================================================================================*/
void unmask_all_lanes()
{
dbg(fDebug, “unmask all lanes”);

for (int n=0; n<NUM_LANES; n++)
{
lane_mask[n] = false;
}

return;
}
/*================================================================================*
SEND DEBUG TO COMPUTER
*================================================================================*/
void dbg(int flag, const char * msg, int val)
{
char tmps[50];
if (!flag) return;

smsg_str(“dbg: “, false);
smsg_str(msg, false);

if (val != -999)
{
sprintf(tmps, “%d”, val);
smsg_str(tmps);
}
else
{
smsg_str(“”);
}

return;
}
/*================================================================================*
SEND SERIAL MESSAGE (CHAR) TO COMPUTER
*================================================================================*/
void smsg(char msg, boolean crlf)
{
if (crlf)
{
Serial.println(msg);
}
else
{
Serial.print(msg);
}

return;
}
/*================================================================================*
SEND SERIAL MESSAGE (STRING) TO COMPUTER
*================================================================================*/
void smsg_str(const char * msg, boolean crlf)
{
if (crlf)
{
Serial.println(msg);
}
else
{
Serial.print(msg);
}

return;
}

A little long for posting here, but I wanted to post any changes I had made in case it helps someone else! Now the timers work, alternating between places and time per track/car! This was a really fun project, and I highly recommend it to anyone looking for a fun electronics project to work on.

I might still dress it up a bit, but it is fully functional as is, now.

Linux – keep it simple.

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 )

Google photo

You are commenting using your Google 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 )

Connecting to %s