Arduino Project

April 2023

Arduino Boy Scout Pinewood Derby Car Race


Functional Requirements

  • Automatically detect the start of the race and provide a visual indication of that detection status.
  • Record the elapsed time for the race and report the final time for each of the three lanes.
  • Detect the Pinewood derby car finish result sequences (1, 2, 3) for three tracks/cars.
  • Display the finsh result places (1, 2, 3) above the finish line with a daylight readable display.
  • Provide a means to reset the system between each race.
  • It must be possible to verify the proper operation of the race detection system prior to starting the race.

Plan

  • The system is either powered on or reset, causing a brief test pattern to be displayed.  
  • If the microswitch is closed during power on / reset, then the system will go into a Test Mode where both colons are on steady, and the display will show a "0" if the IR doesn't detect anything, and "1" if it does.   This will continue until the system is reset and the microswitch is open.  
  • If the microswitch is open during power on / reset, but any of the IR detectors for the three lanes is obstructed, then the sytem will go into Test Mode (must reset to exit test mode).  
  • If the microswitch is open during power on / reset, then "----" will be displayed.   Thereafter, if the microswitch is closed, then the race has started and the display will show a blinking colon ":".  
  • When the first car reaches the finish and interrupts the IR emitter/detector, the display position corresponding to the track position will show a blinking "1" (in addition to the colon).   This will repeat for the 2nd and 3rd cars.  
  • After all three cars have reached the finish line, the 3x places displayed will blink, and the blinking colon removed.  

Hardware

Start of Race Detection

Microswitch ..

Finish Results Display

Adafruit Product #1270 1.2" tall, 4 digit, 7 segment, display.   Communication via I2C (address of 0x70 to 0x77, selectable with jumpers).   It prefers 5V power and 5V logic.   Must set the jumper between the backpack 'IO' pin and +5V to indicate I2C will be 5V logic.   My notes on how to use the display.  

At full brightness, I measured a maximum current consumption of less than 31 mA (spikes may be higher).   The Arduino UNO 5V connection can provide up to 400 mA.   31 mA is too much for a digital output to be used to supply power to the display.  

Adafruit tutorial

Finish Detection

After testing some IR emitter and receivers, the Adafruit 3 mm IR Break Beam Sensor allows the emitter and receiver was chosen because of its robust performance in ambient light, and each pair of emitter / detector was insensitive to a nearby pair.   The emitter/detector pairs may be separated up to 25 cm (10 inches) apart.   Placing the emitter below in the track and then sensing above the track with the detector provides the best isolation from ambient light noise.   The signal from the detector is LOW or 0 VDC when the emitter is not detected, and approximately 2.0 VDC (HIGH) when detected.   This works well with Arduino 5V digital input logic.  

Fritzing parts

Circuit

Power of 5 VDC and at least 500 mA is supplied to the Arduino Uno.   The 5V output from the Arduino provides power to the display, IR emitter, and the IR detector.  

The display communicates with the Arduino via I2C on pins A4/SDA and A5/SCL.  

Each IR detector has a 10k ohm pullup resistor.   This causes the signal to the Arduino to be LOW when IR is not detected, and HIGH when IR is detected.   The IR detector will be HIGH when unobstructed, and then LOW (briefly) when a car passes between the IR emitter and IR detector.  

The microswitch common terminal is connected to 5 VDC, and the other normally open (NO) contact is connected to a 10k ohm pulldown resistor.   The digital input to the Arduino on D6 sees 0 VDC when the switch is open (or LOW), and +5 VDC when the switch is closed.   The resistor keeps the current limited to 0.5 mA (I = V/R = 5.0/10000 = 0.005 A).  

Firmware


/*
  proj_BSA_Pinewood_Derby.ino
  
  Digital inputs:
    Adafruit 3 mm IR Break Beam Sensor PN 2167
    Microswitch 

  3x of Adafruit Product #1270 1.2" tall, 4 digit, 7 segment, display

  In Arduino IDE:
    Set board to 'Arduino Uno'
    Set programmer to 'AVRISP mkii'
    Set COM port
  
*/

/////////////////////////////////////////////////////////////////////////
// Built in LED on D13

const uint8_t pinBuiltInLED = 13;

/////////////////////////////////////////////////////////////////////////
// Adafruit #1270 1.2" tall, 4 digit, 7 segment, display

// Download the "Adafruit LED Backpack" library and the "Adafruit GFX library" from the Arduino library manager.  

#include <Wire.h> // Enable this line if using Arduino Uno, Mega, etc.
#include <Adafruit_GFX.h>
#include "Adafruit_LEDBackpack.h"

Adafruit_7segment matrix1 = Adafruit_7segment();
Adafruit_7segment matrix2 = Adafruit_7segment();
Adafruit_7segment matrix3 = Adafruit_7segment();

void MatrixTest(Adafruit_7segment matrix){
  // A quick test that the 7-segment LED is working.
  matrix.clear(); matrix.writeDisplay();  // Clear the display (includes colon)
  uint8_t counter = 0;
  for (uint8_t d=0; d<25; d++) {
    // paint one LED per row. The HT16K33 internal memory looks like
    // a 8x16 bit matrix (8 rows, 16 columns)
    for (uint8_t i=0; i<8; i++) {
      // draw a diagonal row of pixels
      matrix.displaybuffer[i] = _BV((counter+i) % 16) | _BV((counter+i+8) % 16)  ;
    }
    // write the changes we just made to the display
    matrix.writeDisplay();
    delay(100);
    counter++;
    if (counter >= 16) counter = 0;  
    delay(1);
  }
  matrix.clear(); matrix.writeDisplay();  // Clear the display (includes colon)
} // MatrixTest()

/////////////////////////////////////////////////////////////////////////
// Derby specific hardware

const uint8_t PIN_IR_LANE1 = 12;
const uint8_t PIN_IR_LANE2 = 10;
const uint8_t PIN_IR_LANE3 = 8;

const uint8_t PIN_MICROSW = 6;
uint8_t state_microsw = LOW;
uint8_t state_microsw_last = LOW;
uint8_t state_microsw_boot = LOW;

uint8_t mode_derby = 0;
// 0 = boot / reset
// 1 = test mode
// 2 = awaiting race start
// 3 = race underway
// 4 = race over

uint8_t place_lane1 = 0;
uint8_t place_lane2 = 0;
uint8_t place_lane3 = 0;
uint8_t last_place = 0;

uint32_t race_timer_ms = 0;
uint32_t lane1_timer_s = 0;
uint32_t lane2_timer_s = 0;
uint32_t lane3_timer_s = 0;

void minSecDigits(uint8_t num, uint8_t* tens, uint8_t* ones) {
  // Breaks num into two digits where tens represents the 
  // value divided by ten, and ones is the remainder.
  // Used for a matrix display.
  // Ex.  minSecDigits(9, tens, ones);  tens = 0, ones = 9
  // Ex.  minSecDigits(21, tens, ones);  tens = 2, ones = 1
  uint8_t dig1 = static_cast<uint8_t>(num / 10);
  if (dig1 > 0) {
    *ones = num - (dig1 * 10);
  } else {
    *ones = num;
  }
  *tens = dig1;
} // minSecDigits()


void TestMode(uint8_t state_lane1, uint8_t state_lane2, uint8_t state_lane3) {  
  
  matrix1.clear(); 
  matrix1.drawColon(false);
  matrix1.writeDigitNum(1, state_lane1, false);
  matrix1.writeDisplay();  
  
  matrix2.clear(); 
  matrix2.drawColon(false);
  matrix2.writeDigitNum(1, state_lane2, false);
  matrix2.writeDisplay();  
  
  matrix3.clear(); 
  matrix3.drawColon(false);
  matrix3.writeDigitNum(1, state_lane3, false);
  matrix3.writeDisplay();  
  
} // TestMode()


void RaceStartWait(Adafruit_7segment matrix) {
  matrix.print(12345, DEC); // "----" 
  matrix.writeDisplay();
} // RaceStartWait()


void RaceStarted(Adafruit_7segment matrix) {
  matrix.clear(); matrix.writeDisplay();  // Clear the display
  matrix.blinkRate(1);  // fast blink
  matrix.drawColon(true); matrix.writeDisplay();
} // RaceStarted()


void CarAtFinish(Adafruit_7segment matrix, uint8_t place) {
  matrix.writeDigitNum(1, place, false);
  matrix.blinkRate(0);  // stop blinking
  matrix.drawColon(false);
  matrix.writeDisplay();  
} // TestMode()


void DisplayRaceTime(uint32_t lane_time_s, Adafruit_7segment matrix) {
  matrix.clear();

  uint8_t race_min = static_cast<uint8_t>(lane_time_s / 60);
  //OR: uint8_t race_min = (uint8_t)(lane_time_s / 60);
  uint8_t race_sec = 0;
  //Serial.print(lane_time_s); Serial.print(" s = \t"); Serial.print(race_min); Serial.print(" min\t"); Serial.print(race_sec); Serial.println(" sec");
  if (race_min > 0) {
    race_sec = lane_time_s - (race_min * 60);
  } else {
    race_sec = lane_time_s;
  }
  //Serial.print(lane_time_s); Serial.print(" s = \t"); Serial.print(race_min); Serial.print(" min\t"); Serial.print(race_sec); Serial.println(" sec");
  // Update the seconds on the display
  uint8_t tens = 0;
  uint8_t ones = 0;
  // minSecDigits(uint8_t num, &uint8_t tens, &uint8_t ones)
  minSecDigits(race_sec, &tens, &ones);
  //Serial.print(race_sec); Serial.print("\t"); Serial.print(tens); Serial.print("\t"); Serial.print(ones); Serial.println("\n");
  matrix.writeDigitNum(3, tens, false);   
  matrix.writeDigitNum(4, ones, false);  
  // Update the minutes on the display
  tens = 0; ones = 0;
  minSecDigits(race_min, &tens, &ones);
  matrix.writeDigitNum(0, tens, false);   
  matrix.writeDigitNum(1, ones, false);  
  matrix.drawColon(true);
  matrix.writeDisplay();  
} // DisplayRaceTime()


/////////////////////////////////////////////////////////////////////////


void setup() {
  pinMode(pinBuiltInLED, OUTPUT);

  Serial.begin(9600);
  while (!Serial) {
    digitalWrite(pinBuiltInLED, HIGH);
    delay(1);
  }
  digitalWrite(pinBuiltInLED, LOW);
  Serial.println("\nSerial ready");

  pinMode(PIN_IR_LANE1, INPUT);
  pinMode(PIN_IR_LANE2, INPUT);
  pinMode(PIN_IR_LANE3, INPUT);
  pinMode(PIN_MICROSW, INPUT);

  // Adafruit #1270 1.2" tall, 4 digit, 7 segment, display
  // Set the address below according to the A0, A1, A2 jumpers on the
  // back of the display. 
  matrix1.begin(0x70);   // Initialize the display
  matrix2.begin(0x71);   // Initialize the display
  matrix3.begin(0x73);   // Initialize the display

  // Get the state of PIN_MICROSW during boot
  state_microsw_boot = digitalRead(PIN_MICROSW);

  // Power On / Reset
  //MatrixTest(matrix1); // Show a test pattern
  //MatrixTest(matrix2); // Show a test pattern
  //MatrixTest(matrix3); // Show a test pattern
  
  Serial.println("Setup complete\n");
} // setup()


void loop() {

  uint8_t state_lane1 = digitalRead(PIN_IR_LANE1);
  uint8_t state_lane2 = digitalRead(PIN_IR_LANE2);
  uint8_t state_lane3 = digitalRead(PIN_IR_LANE3);
  
  if (mode_derby == 0 && state_microsw_boot == HIGH) {
    mode_derby = 1;  // test mode
    Serial.println("Test mode");
  } else if (mode_derby == 0 && state_microsw_boot == LOW) {
    if (state_lane1 == HIGH && state_lane2 == HIGH && state_lane3 == HIGH) {
      // None of the IR emitter/detector lanes are obstructed.
      mode_derby = 2;  // awaiting race start
      Serial.println("Awaiting race start");          
      RaceStartWait(matrix1);
      RaceStartWait(matrix2);
      RaceStartWait(matrix3);
    } else {
      // One or more of the IR emitter/detector lanes is obstructed.  Go into Test Mode.
      mode_derby = 1;  // test mode
      Serial.println("IR ERROR - now in test mode");      
    }
  }

  if (mode_derby == 1) {
    // test mode  (it will not get out of test mode until restart)
    TestMode(state_lane1, state_lane2, state_lane3);
  } // test mode

  state_microsw = digitalRead(PIN_MICROSW);
  // Detect a change in the state of the microswitch (LOW to HIGH)
  if (state_microsw != state_microsw_last && state_microsw == HIGH) {
    // state_microsw == HIGH && state_microsw_last == LOW
    state_microsw_last = HIGH;
  }

  if (mode_derby == 2 && state_microsw == HIGH) {
    // race underway
    race_timer_ms = millis();
    mode_derby = 3;  // race underway
    RaceStarted(matrix1);
    RaceStarted(matrix2);
    RaceStarted(matrix3);
  }
  
  if (mode_derby == 3) {
    // race underway
    digitalWrite(pinBuiltInLED, HIGH);
    if (place_lane1 > 0 && place_lane2 > 0 && place_lane3 > 0) {
      mode_derby = 4; // race over
    }
    if (last_place >= 3) {
      mode_derby = 4; // race over
    }
    if (place_lane1 == 0 && state_lane1 == LOW) {
      // A car has briefly obstructed the IR emitter/detector at Lane #1
      last_place++;
      place_lane1 = last_place;
      lane1_timer_s = (millis() - race_timer_ms) / 1000;
      //Serial.print("\tplace_lane1: "); Serial.print(place_lane1); Serial.print("\tTime [s]: "); Serial.println(lane1_timer_s);
      CarAtFinish(matrix1, place_lane1);
    }
    if (place_lane2 == 0 && state_lane2 == LOW) {
      // A car has briefly obstructed the IR emitter/detector at Lane #2
      last_place++;
      place_lane2 = last_place;
      lane2_timer_s = (millis() - race_timer_ms) / 1000;
      //Serial.print("\tplace_lane2: "); Serial.print(place_lane2); Serial.print("\tTime [s]: "); Serial.println(lane2_timer_s);
      CarAtFinish(matrix2, place_lane2);
    }
    if (place_lane3 == 0 && state_lane3 == LOW) {
      // A car has briefly obstructed the IR emitter/detector at Lane #3
      last_place++;
      place_lane3 = last_place;
      lane3_timer_s = (millis() - race_timer_ms) / 1000;
      //Serial.print("\tplace_lane3: "); Serial.print(place_lane3); Serial.print("\tTime [s]: "); Serial.println(lane3_timer_s);
      CarAtFinish(matrix3, place_lane3);
    }
  } 
  
  if (mode_derby == 4) {
    // Race over
    digitalWrite(pinBuiltInLED, LOW);
    // Show race time
    DisplayRaceTime(lane1_timer_s, matrix1);
    DisplayRaceTime(lane2_timer_s, matrix2);
    DisplayRaceTime(lane3_timer_s, matrix3);
    while (true) {}
  }

} // loop()

 


Do you need help developing or customizing a IoT product for your needs?   Send me an email requesting a free one hour phone / web share consultation.  

 

The information presented on this website is for the author's use only.   Use of this information by anyone other than the author is offered as guidelines and non-professional advice only.   No liability is assumed by the author or this web site.