IR Break-Beam Stopwatch

Updated 29 April 2022

This project is a simple stopwatch with a little bit of a twist! It operates like a conventional stopwatch… you can start and stop it, you can record lap times and you can review the lap times once you hit stop.

The twist is that this stopwatch has an IR break-beam sensor. When the beam is broken by (for example) a person running through the beam, it will trigger the Arduino to record a lap time!

For this project, you will need

  • 1x Any colour 5mm LED (I recommend this Superbright LED for outdoor use).

Breadboard layout and theory

breadboard-layout-and-mounted-components

The IR LED needs to be bent on a 90 degree angle so it is aimed horizontally.  I use ‘blue-tack’ to prop a plastic tube in place to shield the IR sensor from the IR LED. The light from the IR LED is then aimed at a prismatic reflector 2 meters away.  This reflects the IR light back to the IR receiver.

If you have a dog and you are a responsible pet owner, you may be familiar with the black plastic tubes which dog-poo tidy bags are rolled onto.  These tubes are IDEAL for shielding a 5mm LED!!!

The orange wire is connected to the digital pin 2 on the keypad shield.  This connects straight to pin D2 on the Arduino….. it is used for detecting a change in the signal from the IR receiver.

The yellow wire is connected to digital pin 3 on the keypad shield (access to pin D3 on the Arduino).  This pin is used to generate a Pulse Width Modulation signal at a frequency of 38kHz.

The TSOP38238 by default outputs a HIGH signal when it does not detect a 38kHz IR signal.  When the IR light is exposed to it, the output from the TSOP38238 goes LOW.

NOTE:- the high signal is a lower voltage.  This would be fine connecting to a 3.3 volt microcontroller, but the Arduino uses 5 Volt logic.  To get around this, I use a 5 volt circuit going through an NPN transistor.  This allows a 5 Volt circuit to go through the indicator LED making it brighter, and I also use this circuit to connect to D2 for reading the signal state.  The output from the IR receiver is connected to the base pin of the NPN transistor.

The 2nd circuit is the PWM for the IR LED.  According to the datasheet for the IR LED, it can handle a constant current of 100mA.  When you pulse the current, the IR LED can handle up to 1 Amp!

For my example, I’m using a 10 ohm resistor.  Ohms law:-

V = IR

5 Volts = I x 10 ohms

I = 5 volts / 10 ohms = 0.5 amp

I = 500mA

This is half what the IR LED can handle when pulsed.  When I tested it out, I could get a distance of 2 meters away from the IR receiver before it couldn’t detect the reflected light.

Theoretically, if you used a 2nd microcontroller which simply pulsed an IR LED, and you aimed it directly at the Arduino with the IR receiver, you could get near DOUBLE the distance for the beam.  I haven’t actually tested this out though, I use a prismatic reflector so that all the electronics are kept together on one side.

In this project I’m using NPN transistors with a current limit of 800mA, so at 500mA, there won’t be any problems.

If you wanted to run a current closer to 1 Amp, you MUST use a transistor with a higher current capacity.  For example this 2N5191G from Core Electronics.


Circuit Diagram

circuit-diagram-led-driver

digital-breadboard-connections-featuring-all-of-the-components

Photos of the assembled project

assembled-breakbeam-stopwatch

Code

Before you upload this code you will need to install the PWM library, download the .zip file in the attachments below and include it in your sketch! (A video on how to install a library)

 

Copy the following code to the Arduino IDE and upload to your Arduino board:

/*
 * An Arduino stopwatch which records up to 20 lap times.
 * Uses the DFROBOT LCD keypad version 1.1.  Serial number DFR0009.
 * Press SELECT to toggle the start/stop of the timer.
 * Press LEFT to record a lap time.
 * After finishing a timing session, use the UP and DOWN buttons to cycle
 * through the recorded lap times.  They will display at the bottom of the LCD.
 * The TOTAL time from start to finish will be displayed at the top of the LCD.
 * You can also use the Serial Monitor to run this stopwatch.
 * 
 * PWM.h library download link:- http://code.google.com/p/arduino-pwm-frequency-library/downloads/list
 */

/* USING THE KEYPAD BUTTONS ON THE DFROBOT LCD KEYPAD SHIELD ver 1.1
 * -----------------------------------------------------------------
 * ALL 5 buttons are connected to analog pin A0.
 * When no buttons are pressed, the analog value on A0 is 1 (1023).
 * 
 * Analog values for the buttons are:-
 * 
 * SELECT:-   Between 800 and 900.
 * LEFT:-     Between 590 and 650.
 * DOWN:-     Between 370 and 430.
 * UP:-       Between 170 and 230.
 * RIGHT:-    0.
 */
 
// Include necessary libraries.
# include 
# include 
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);    // Pins used for LCD panel.

// Declare constant pin for pulsing the Superbright IR LED.
// Only pwm pins 3 and 11 are available when the DFROBOT shield is fitted to the Arduino.
// Testing shows that PWM.h works with pins 3, 9 and 10.
const int pulsePin = 3;

// Signal from the tsop382 IR receiver.  Signal is HIGH (about 3.0 volts) when no 
// 38kHz IR signal is detected.  Goes LOW when the IR beam is shining on it.
const int irPin = 2;             

// Use this function of PWM.h to set the frequency for pin 3 to 38kHz.  This is the 
// frequency the IR receiver is notched to work with.
int32_t frequency = 38000;              // The frequency is written here in Hz.

// Declare a variable needed for updating the LCD time display.
unsigned long lcdUpdateTime = 0;

// Declare variables for handling debouncing input buttons.
// Keep the debounceDelay as low as possible for better timer accuracy.
unsigned long ButtonPressTime = 0;      // The millis when the start/stop button is pressed.
unsigned long debounceDelay = 100;      // The delay time in ms for the button.
                                        
// Assign which analog pins to use for the buttons.
bool timeRunning =  false;              // True if the stopwatch is running. False if not.

// Setting up an array of size 20 with a lap counter.
// The idea is that the millis() when start, lap or stop are pressed will be stored straight
// into the array.  These are the event time markers.
int lapCount =  0;

/* The start time is always stored in the first element of the array (position 0), so to 
 * record 20 lap times, you need to create an array of 21 elements.
 * If you want to record more/less laps, you can change the listSize variable.
 */
const int listSize = 21;
unsigned long lapList[listSize]; 

// Use this to help refresh the lap results on the LCD when UP or DOWN are pressed.
// It is the total number of laps recorded.  This will be copied from lapCount when
// the stopwatch is stopped.
int maxLap = 0;                     

/* Declare variables for converting millis to a readable time.  These are GLOBAL because
 * There are 2 different display functions which will use the formatTime() function to 
 * convert millis to actual time.  Saves on doubling-up on code writing.
 */ 
float h, m, s;
int ms;
unsigned long remainder;

void formatTime(unsigned long num) {

  /* Convert the lap times into a more readable form.
   * Some useful things to know here.....
   * 
   * 1 Hour = 60 minutes OR 3600 seconds OR 3,600,000 milliseconds.
   * 1 Minute = 60 seconds OR 60,000 milliseconds.
   * 1 Second = 1000 milliseconds.
   */

  h = num / 3600000;                          // Extract num of hours from time.
  remainder = num % 3600000;                  // Get the remainder from hour.
  m = remainder / 60000;                      // Extract num for minutes from hour remainder.
  remainder = remainder % 60000;              // Get remainder from minutes.
  s = remainder / 1000;                       // Extract num of seconds from minutes remainder.
  ms = remainder % 1000;                      // The remainder from the seconds IS the correct milliseconds.
                              
}

/*-------------------- The time display for the Serial monitor function ----------------------*/
void displaySerial() {
  if (h < 10) {
    Serial.print("0");                 
  }
  Serial.print(h, 0);                   
  Serial.print(":");
  if (m < 10) {
    Serial.print("0");
  }
  Serial.print(m, 0);
  Serial.print(":");
  if (s < 10) {
    Serial.print("0");
  }
  Serial.print(s, 0);
  Serial.print(".");
  if (ms > 10 && ms < 100) {
    Serial.print("0");
  }
  if (ms < 10) {
    Serial.print("00");
  }
  Serial.println(ms);             // ms value already 3 digits by default so leave 0 for digits.
}

/*-------------------- The time display for the LCD screen ----------------------------------*/
void displayLcd(int rowNum) {

  if (rowNum == 0) {
    lcd.setCursor(2, 0);          // First digit is for column, second digit is for row on LCD screen. 
  }
  else if (rowNum == 1) {         // Top row for updated timer, bottom row for displaying lap times.
    lcd.setCursor(0, 1);
    lcd.print("L");

    if (lapCount > 0) {
      lcd.setCursor(1, 1);
      if (lapCount < 10) {
        lcd.print(" ");
      }
      lcd.print(lapCount);
    }

    lcd.setCursor(4, 1);
  }
  
  if (h < 10) {
    lcd.print("0");
  }
  lcd.print(h, 0);
  lcd.print(":");
  if (m < 10) {
    lcd.print("0");
  }
  lcd.print(m, 0);
  lcd.print(":");
  if (s < 10) {
    lcd.print("0");
  }
  lcd.print(s, 0);
  lcd.print(".");
  if (ms < 100) {
    lcd.print("0");
  }
  if (ms < 10) {
    lcd.print("0");
  }
  lcd.print(ms);           
}

void calculateLap() {

  /* The latest time in millis has just been stored in the lapList array.
   * If you subtract the lapList array value in the previous slot from the 
   * most recent slot, you should get the time in milliseconds for the 
   * most recent lap.
   */

  unsigned long lapTime = lapList[lapCount] - lapList[lapCount - 1];
  formatTime(lapTime);

  Serial.print("Lap ");
  if (lapCount < 10) {
    Serial.print(" ");
  }
  Serial.print(lapCount);
  Serial.print(":-    ");
  displaySerial();
  displayLcd(1);                        // 0 means print to top row, 1 means print on bottom row.
}

void resetTime() {
  // Set all the values in the lapList array to 0, ready for the next cycle.
  for (int i = 0; i <= listSize; i++) {
    lapList[i] = 0;
    lcd.setCursor(0, 1);
    lcd.print("                ");      // Clears the laptime display ready for a new set of times.
  }   

  lapCount = 0;
  lcdUpdateTime = 0;
}

void stopTimer() {
  
  // Stop the active stopwatch by changing the boolean value to false.
  timeRunning = false;

  /* The final value of lapCount when the stopwatch has been stopped is stored in
   * this variable.  The lapCount variable will be manipulated to recall previous lap
   * recordings.
   */
  maxLap = lapCount;
  
  // Assuming the final lap is completed when stop is pressed,
  // Call calculateLap() to work out the final lap time.
  calculateLap();

  /* Subtract the final millis in the lapList from the first millis in the lapList,
   * this gives the total time from start to finish, this will be displayed in the
   * serial monitor.  This total SHOULD match the time on the LCD display when the
   * timer was stopped.
   */ 
  unsigned long totalTime = lapList[lapCount] - lapList[0];
 
  Serial.println("\nTimer stopped!");
  Serial.print("\nThe total time between start and finish is :- ");
  formatTime(totalTime);
  displaySerial();                                      
  displayLcd(0);                        // This will 'match' the LCD display with the total in the Serial Monitor.
}

void setup() {

  pinMode(irPin, INPUT_PULLUP);         // Will return a 1 when the IR beam is detected, 0 if the IR beam is broken.

  InitTimersSafe();                     // Enables the Arduino timer frequency (excpet Timer 0) to be modified.

  // PWM.h function to set the frequency on pin3 to 38kHz.
  SetPinFrequencySafe(pulsePin, frequency);

  // Begin the pulsing on pin 3, 50% duty cycle.  This function works much the same as analogWrite().
  // Duty cycle scale:- 0 is (obviously) 0, 255 is full 5 volts.
  pwmWrite(pulsePin, 128);
  
  // Enable the LCD display and the serial monitor
  lcd.begin(16, 2);
  Serial.begin(9600);
  
  formatTime(0);
  displayLcd(0);
  Serial.println("Press the SELECT button on the keypad to toggle start/stop.");
  Serial.println("Press the LEFT button on the keypad to record a lap.");
}

void loop() {
  
  // Read the values of the start/stop and the lap buttons.
  float ButtonVal = analogRead(A0);

  // Read the state of the IR beam.  Beam broken returns a 0, beam received returns a 1.
  int irSignal = digitalRead(irPin);
  
            /* ---- The START/STOP button (SELECT) ----*/
  // Voltage value goes above 1010 when SELECT button is pressed.
  // If SELECT button is pressed AND IR beam is detected... this prevents the timer from
  // being activated if the IR beam is not correctly aligned with the IR sensor.
  if ((ButtonVal > 800 && ButtonVal < 900) && irSignal == 1) {
    if((millis() - ButtonPressTime) > debounceDelay) {
      if (timeRunning == false) {
        // Clear the array of any previous time data before the new timing run.
        resetTime();
        // ALL the millis time markers will be stored in the lapList array.
        // Therefore, the starting time will always be in lapList[0].
        lapList[lapCount] = millis();
        lcdUpdateTime = millis();
        lapCount++;
        
        Serial.println("\nTimer started.....\n");
        // timeRunning is now made 'true' since the timer has started running.
        timeRunning = true;
      }
      
      else if (timeRunning == true) {
        lapList[lapCount] = millis();
        lcdUpdateTime = millis() - lapList[0];
        stopTimer();
        Serial.print("\n");
        Serial.println("Pressing UP and DOWN on the LCD keypad will scroll through the lap times\n");
        Serial.println("Press the SELECT button on the keypad to toggle start/stop.");
        Serial.println("Press the LEFT button on the keypad to record a lap.");
      }
    }
    ButtonPressTime = millis();
  }

            /* ---- The LAP button (LEFT) ----*/
  // If the LEFT button is pressed OR the IR beam is broken: 
  if ((ButtonVal > 590 && ButtonVal < 650) || irSignal == 0) {
    if((millis() - ButtonPressTime) > debounceDelay) {
      if (timeRunning == true) {
        // If there is still room in the lapList array, add the millis marker for
        // when the lap button is pressed.
        if (lapCount < (listSize - 1)) {
          lapList[lapCount] = millis();
          calculateLap();
          lapCount++;
        }
        else {
          // If you have reached the maximum number of laps, record the final millis
          // and stop the timer.
          lapList[lapCount] = millis();
          stopTimer();
        }
      }
    }
    ButtonPressTime = millis();
  }

            /* ---- The SCROLL DOWN button (DOWN) ----*/
  if (ButtonVal > 370 && ButtonVal < 430) {
    if((millis() - ButtonPressTime) > debounceDelay) {
      if (timeRunning == false && lapCount > 0 && lapCount <= maxLap) {
        // Add 1 to the lapCount value, calculate what the previous lap was.
        lapCount++;

        // Needed to stop the user scrolling past the last lap time in the array.
        if (lapCount == maxLap + 1) {
          lapCount = maxLap;
        }
        
        unsigned long lapTime = lapList[lapCount] - lapList[lapCount - 1];
        // Convert lapTime to h, m, s, ms (again), display lap time on LCD.
        formatTime(lapTime);
        displayLcd(1);
      }
    }
    ButtonPressTime = millis();
  }

            /* ---- The SCROLL UP button (UP) ----*/
  if (ButtonVal > 170 && ButtonVal < 230) {
    if((millis() - ButtonPressTime) > debounceDelay) {
      if (timeRunning == false && lapCount > 0 && lapCount <= maxLap) {
        // Subtract 1 from the lapCount value, display lap time n LCD.
        lapCount--;

        // Needed to stop the user scrolling past the first time in the array.
        if (lapCount == 0) {
          lapCount = 1;
        }
        unsigned long lapTime = lapList[lapCount] - lapList[lapCount - 1];
        // Convert lapTime to h, m, s, ms (again), display lap time on LCD.
        formatTime(lapTime);
        displayLcd(1);
      }
    }
    ButtonPressTime = millis();
  }
  
//  These conditions will only run when the boolean timeRunning is TRUE.
  if (timeRunning == true) {
    // Update the timer display every millisecond while running.
    lcdUpdateTime = millis() - lapList[0];
    formatTime(lcdUpdateTime);
    displayLcd(0);

    // If the timer ran for an entire day, it will stop.
    if (h == 24) {
      lapList[lapCount] = millis();
      stopTimer();
    }
  }
}

Testing the project

With the code uploaded to your Arduino, hold the prismatic reflector in front of the IR LED.  If all goes well, the indicator LED should go out.

breakbeam-stopwatch-setup-with-reflector

Fix the prismatic reflector in position so that IR light is getting back to the receiver.  If you run your hand through the path of the IR light, you should see the indicator LED briefly light up before going off again.

To operate the stopwatch, press the SELECT button on the keypad to toggle the start/stop function.  You can press the LEFT button to record a lap time manually OR you can break the IR beam to record a lap time.

When the timer has been stopped, you can use the UP and DOWN buttons on the keypad to review the lap times that were recorded.  These will be wiped clean if you start the stopwatch again.

 

Conclusion

This gives you a simple yet practical stopwatch that can be used in outdoor applications.  A possible hack:- the code could be modified so the first break of the IR beam will start the timer.  If you know exactly how many laps you want to do, change the value listSize in the code to the number of laps you want (just remember to add 1 to the number of laps you want, it has to do with the functioning of the array in the code).

This would automatically stop the timer on the completion of the last lap.

Have fun with your new lap timer!

testing-the-stopwatch

Additional: My original timer project - Flyball racing timer

an-adaptation-of-the-breakbeam-stopwatch-the-flyball-timer

Flyball is a sport for dogs.  The dogs run in a 4 dog relay. They run over 4 hurdle-style jumps, jump on a ‘box’ which fires a ball out for them to catch.  They run back down over the jumps to their handler and the next dog in the relay runs.

A race has 2 teams racing to see which one is the fastest (but the slower team can win if the faster team manage to stuff something up!).

For a better idea about flyball, it would be best to watch a YouTube video of a flyball race.

At the competitions, they use an electronic timer system to record the dogs times.  This equipment is EXPENSIVE to say the least.

My challenge was to build an affordable, scaled-down version of a flyball timer.  My timer works as a training module as it works with one lane only.

another-angle-of-the-flyball-timer

Another angle showing the prismatic reflector post.

I’ll give a brief outline of this project:-

  • It uses an Raspberry Pi 3B+ for recording the timing.  The results are shown on a 7 inch LED display.

  • There are 2 IR break-beam posts, each using 4 IR break-beam sensors.  This is because dogs come in different heights.

  • The first post when a dog breaks the beam will trip the timer to record a time.  The second post is used to ‘check’ that the running dog has returned before the next dog in the relay can go.

  • If the next dog enters the run before the current dog has returned, a buzzer will sound and the red light will switch on for 3 seconds.

  • The 2 IR break-beam poles are controlled by an Arduino Uno.  When a beam is broken, the appropriate HIGH/LOW signal is sent to a RPI GPIO pin via a simple voltage divider.

  • It is powered with a 12 Volt sealed lead acid battery, using DC-DC step down modules to reduce the voltage to a safe level for the Pi, Arduino, IR gateway and the light tree.

  • The pictures show the project with the audio speakers, but I have removed them and replaced them with an industrial grade piezo buzzer.  The sound is not as pleasant, but it is LOUD.  You need loud when you have a bunch of barking dogs going nuts!

  • The timer can be started remotely using a doorbell!.  I hacked a 433mHZ doorbell kit from Bunnings.  I extracted the receiver unit from the doorbell and identified which pin activates with a press of the button.  This simply goes to a GPIO on the Pi, and a high signal is interpreted as “Start”. 

early-gui-for-flyball-timer

Creating the GUI in the early stages of the Flyball timer project.

Have a question? Ask the Author of this guide today!

Please enter minimum 20 characters

Your comment will be posted (automatically) on our Support Forum which is publicly accessible. Don't enter private information, such as your phone number.

Expect a quick reply during business hours, many of us check-in over the weekend as well.

Comments


Loading...
Feedback

Please continue if you would like to leave feedback for any of these topics:

  • Website features/issues
  • Content errors/improvements
  • Missing products/categories
  • Product assignments to categories
  • Search results relevance

For all other inquiries (orders status, stock levels, etc), please contact our support team for quick assistance.

Note: click continue and a draft email will be opened to edit. If you don't have an email client on your device, then send a message via the chat icon on the bottom left of our website.

Makers love reviews as much as you do, please follow this link to review the products you have purchased.