Community project

Maak Een Loop Timinsysteem Met Df Mijni En

Felix Spelletjes

Published July 21, 2026

Arduino7 components6 assembly steps
Remix this project
Photo of Maak Een Loop Timinsysteem Met Df Mijni En

Build a loop timer system that uses a laser beam and light sensor to detect when a runner crosses the finish line. This Arduino-based timer combines a DFPlayer Mini audio module for sound feedback, a red laser module creating a light gate, and a photoresistor to sense beam interruption. The guide provides a complete wiring diagram, parts list, calibration procedure, and Arduino firmware with configurable timing parameters.

Assembly involves connecting the start button, laser module, and light sensor to form an optical gate, then integrating the DFPlayer Mini and speaker for audio announcements. The firmware handles debouncing, threshold calibration, and state management to accurately measure lap times. Customize sensitivity, lead times, and audio volume through serial commands before running your first timed loop.

Wiring diagram

Interactive · read-only
Wiring diagram for Maak Een Loop Timinsysteem Met Df Mijni En

Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.

Parts list

Bill of materials
ComponentQtyNotes
DFPlayer Mini MP3 Audio ModuleMP3 player1Small UART-controlled MP3 playback module with a microSD card slot and onboard audio DAC/amplifier interface. The MCU controls playback over serial while speaker/DAC pins connect to the audio output path.
Momentary start pushbuttonNormally-open1Normally-open momentary pushbutton used to arm and start a run.
5 V red laser module5 V, low-power red1Low-power red laser module that continuously illuminates the finish-line LDR.
Photoresistor (LDR)GL5528 or equivalent1Light-dependent resistor used with a fixed resistor as the laser-beam finish sensor voltage divider.
10 kΩ resistor10 kΩ1Fixed lower resistor in the LDR voltage divider, from the LDR sensing node to ground.
1 kΩ resistor1 kΩ1Series resistor protecting/conditioning the DFPlayer RX UART input from the Uno 5 V transmit signal.
8 Ω 3 W speaker8 Ω, 3 W1Small passive speaker driven directly from DFPlayer Mini SPK1/SPK2 outputs.

Assembly

6 steps
  1. Schakel alles uit en maak een gezamenlijke massa

    Plaats de Arduino Uno, DFPlayer Mini, startknop, laser, LDR, weerstanden en luidspreker op een breadboard. Verbind alle GND-aansluitingen met de GND-rail van de Uno.

    • Tip: Gebruik één GND-rail; zonder gedeelde massa werken de sensor en DFPlayer niet betrouwbaar.
    • Tip: Voed dit project via de USB-aansluiting van de Uno.
    • Sluit de voeding pas aan nadat alle bedrading is gecontroleerd.
  2. Sluit de startknop aan

    Verbind één aansluiting van de normaal-open startknop met Uno D2 en de andere aansluiting met GND. De firmware gebruikt de interne pull-upweerstand, dus er is geen externe knopweerstand nodig.

    • Tip: Een druk op de knop maakt D2 tijdelijk met GND verbonden.
  3. Bouw de laser-lichtsluis

    Verbind de rode lasermodule met 5V en GND. Verbind één poot van de LDR met 5V en de andere met Uno A0. Verbind de 10 kΩ-weerstand tussen diezelfde A0/LDR-knoop en GND.

    • Tip: Richt de laser precies op de LDR en scherm de LDR af met een kort zwart kokertje tegen omgevingslicht.
    • Tip: De laserwaarde moet duidelijk dalen wanneer iemand de laser onderbreekt.
    • Kijk nooit in de laserstraal en richt hem niet op ogen, verkeer of reflecterende oppervlakken. Gebruik uitsluitend een laagvermogen-lasermodule.
  4. Sluit de DFPlayer en luidspreker aan

    Verbind DFPlayer VCC met Uno 5V en DFPlayer GND met GND. Verbind DFPlayer TX met Uno D11. Verbind Uno D10 via de losse 1 kΩ-weerstand met DFPlayer RX. Verbind een 8 Ω-luidspreker uitsluitend tussen DFPlayer SPK1 en SPK2.

    • Tip: Plaats de 1 kΩ-weerstand in serie: D10 → weerstand → DFPlayer RX.
    • Tip: Voor geluid moet een microSD-kaart in de DFPlayer zitten met het startsignaal als 0001.mp3; 0002.mp3 is optioneel als finishgeluid.
    • Verbind SPK1 of SPK2 nooit met GND; dit zijn de twee versterkte, zwevende luidsprekeruitgangen.
    • Gebruik geen hoofdtelefoon op SPK1/SPK2.
  5. Kalibreer vóór de eerste ronde

    Flash de firmware met Schematik Deploy en open daarna src/timer_gui.py op je Linux-computer. Verbind de GUI met de Uno. Richt de laser op de LDR, zorg dat niets de straal onderbreekt en klik op ‘Kalibreer laser’. De gemeten lichtwaarde en de berekende finishdrempel verschijnen in de GUI en blijven bruikbaar na herstart.

    • Tip: Herhaal kalibreren wanneer de laser, LDR of omgevingslicht verandert.
    • Tip: Een kort zwart kokertje rond de LDR vermindert invloed van zon- en kamerlicht.
    • Kalibreer alleen met een vrije laserstraal; anders ontstaat een te lage en onbetrouwbare finishdrempel.
  6. Stel gevoeligheid en timing in

    Pas in de GUI naar wens de drempel, het aantal vereiste donkere metingen, de startvertraging, blokkeertijd, resultaatduur, knop-ontdendering en DFPlayer-volume aan. Klik op ‘Pas alle instellingen toe en bewaar’. Iedere geldige waarde wordt direct en blijvend in het EEPROM-geheugen van de Uno opgeslagen.

    • Tip: Verlaag ‘Finishdrempel (%)’ als de finish te gevoelig is voor omgevingslicht.
    • Tip: Verhoog ‘Benodigde donkere metingen’ als korte storingen een valse finish geven.
    • Tip: Gebruik ‘Herstel standaardwaarden’ in de GUI om terug te gaan naar veilige beginwaarden.
    • Koppel USB los voordat u bedrading verplaatst.

Pin assignments

Board wiring reference
PinConnectionType
5Vdfplayer_1 VCCpower
GNDdfplayer_1 GNDground
GPIO 11dfplayer_1 TXuart
EXTdfplayer_1 RX1 kΩ resistor Buart
EXTdfplayer_1 SPK18 Ω 3 W speaker SPK+data
EXTdfplayer_1 SPK28 Ω 3 W speaker SPK-data
GPIO 10dfplayer_rx_resistor_1 Adigital
GPIO 2start_button_1 SIGdigital
GNDstart_button_1 GNDground
5Vlaser_1 VCCpower
GNDlaser_1 GNDground
5Vldr_1 VCCpower
GPIO 14ldr_1 SENSEanalog
EXTldr_resistor_1 APhotoresistor (LDR) SENSEanalog
GNDldr_resistor_1 Bground

Firmware

Arduino
main.cppDeploy to device
#include <Arduino.h>
#include <EEPROM.h>
#include <SoftwareSerial.h>
#include <DFRobotDFPlayerMini.h>


// Hoisted type definitions
enum TimerState { READY, STARTING, RUNNING, FINISHED };

struct TimerConfig {
  uint16_t magic;
  uint16_t debounceMs;
  uint16_t startLeadMs;
  uint16_t finishArmMs;
  uint16_t resultHoldMs;
  uint8_t darkSamples;
  uint8_t thresholdPercent;
  uint16_t minimumThreshold;
  uint8_t volume;
};


// Forward declarations
void setDefaults();
void saveConfig();
void loadConfig();
int readLightAverage(uint8_t samples);
void calculateThreshold();
void sendConfig();
void reportReady();
void calibrate();
bool setValue(const char *key, long value);
void processCommand();
bool startButtonPressed();
void beginStartSequence();
void beginRun();
void finishRun(int light);

const uint8_t START_BUTTON_PIN = 2;
const uint8_t DFPLAYER_TX_PIN = 10;
const uint8_t DFPLAYER_RX_PIN = 11;
const uint8_t LDR_PIN = A0;
const uint16_t CONFIG_MAGIC = 0x4C54;



TimerConfig config;
TimerState state = READY;
SoftwareSerial dfPlayerSerial(DFPLAYER_RX_PIN, DFPLAYER_TX_PIN);
DFRobotDFPlayerMini dfPlayer;
bool dfPlayerReady = false;
unsigned long stateChangedAt = 0, runStartedAt = 0, lastButtonChangeAt = 0;
bool lastButtonReading = HIGH, stableButtonState = HIGH;
int beamValue = 0, finishThreshold = 0;
uint8_t darkSampleCount = 0;

void setDefaults() {
  config.magic = CONFIG_MAGIC;
  config.debounceMs = 35;
  config.startLeadMs = 1000;
  config.finishArmMs = 250;
  config.resultHoldMs = 2500;
  config.darkSamples = 3;
  config.thresholdPercent = 70;
  config.minimumThreshold = 20;
  config.volume = 22;
}

void saveConfig() { EEPROM.put(0, config); }

void loadConfig() {
  EEPROM.get(0, config);
  if (config.magic != CONFIG_MAGIC) { setDefaults(); saveConfig(); }
}

int readLightAverage(uint8_t samples) {
  long total = 0;
  for (uint8_t i = 0; i < samples; ++i) { total += analogRead(LDR_PIN); delay(2); }
  return total / samples;
}

void calculateThreshold() {
  finishThreshold = (beamValue * (long)config.thresholdPercent) / 100L;
  if (finishThreshold < config.minimumThreshold) finishThreshold = config.minimumThreshold;
  if (finishThreshold > 1023) finishThreshold = 1023;
}

void sendConfig() {
  Serial.print(F("EVENT,CONFIG,"));
  Serial.print(config.debounceMs); Serial.print(',');
  Serial.print(config.startLeadMs); Serial.print(',');
  Serial.print(config.finishArmMs); Serial.print(',');
  Serial.print(config.resultHoldMs); Serial.print(',');
  Serial.print(config.darkSamples); Serial.print(',');
  Serial.print(config.thresholdPercent); Serial.print(',');
  Serial.print(config.minimumThreshold); Serial.print(',');
  Serial.println(config.volume);
}

void reportReady() {
  Serial.print(F("EVENT,READY,")); Serial.print(beamValue); Serial.print(','); Serial.println(finishThreshold);
}

void calibrate() {
  beamValue = readLightAverage(40);
  calculateThreshold();
  saveConfig();
  Serial.print(F("EVENT,CALIBRATED,")); Serial.print(beamValue); Serial.print(','); Serial.println(finishThreshold);
  reportReady();
}

bool setValue(const char *key, long value) {
  if (!strcmp(key, "debounce") && value >= 5 && value <= 500) config.debounceMs = value;
  else if (!strcmp(key, "lead") && value >= 0 && value <= 10000) config.startLeadMs = value;
  else if (!strcmp(key, "arm") && value >= 0 && value <= 10000) config.finishArmMs = value;
  else if (!strcmp(key, "hold") && value >= 0 && value <= 20000) config.resultHoldMs = value;
  else if (!strcmp(key, "samples") && value >= 1 && value <= 20) config.darkSamples = value;
  else if (!strcmp(key, "percent") && value >= 1 && value <= 99) config.thresholdPercent = value;
  else if (!strcmp(key, "minimum") && value >= 0 && value <= 1023) config.minimumThreshold = value;
  else if (!strcmp(key, "volume") && value >= 0 && value <= 30) { config.volume = value; if (dfPlayerReady) dfPlayer.volume(config.volume); }
  else return false;
  calculateThreshold();
  saveConfig();
  return true;
}

void processCommand() {
  static char line[48];
  static uint8_t length = 0;
  while (Serial.available()) {
    char c = Serial.read();
    if (c == '\r') continue;
    if (c == '\n') {
      line[length] = 0;
      if (!strcmp(line, "GET_CONFIG")) sendConfig();
      else if (!strcmp(line, "CALIBRATE") && state == READY) calibrate();
      else if (!strcmp(line, "DEFAULTS") && state == READY) { setDefaults(); saveConfig(); calculateThreshold(); sendConfig(); }
      else if (!strncmp(line, "SET,", 4)) {
        char *key = line + 4;
        char *comma = strchr(key, ',');
        if (comma) { *comma = 0; if (setValue(key, atol(comma + 1))) { Serial.println(F("EVENT,SAVED")); sendConfig(); } else Serial.println(F("EVENT,ERROR,ongeldige waarde")); }
      }
      length = 0;
    } else if (length < sizeof(line) - 1) line[length++] = c;
  }
}

bool startButtonPressed() {
  bool reading = digitalRead(START_BUTTON_PIN);
  if (reading != lastButtonReading) { lastButtonChangeAt = millis(); lastButtonReading = reading; }
  if (millis() - lastButtonChangeAt >= config.debounceMs && reading != stableButtonState) { stableButtonState = reading; return stableButtonState == LOW; }
  return false;
}

void beginStartSequence() {
  beamValue = readLightAverage(20); calculateThreshold(); darkSampleCount = 0;
  if (dfPlayerReady) dfPlayer.play(1);
  state = STARTING; stateChangedAt = millis();
  Serial.print(F("EVENT,COUNTDOWN,")); Serial.print(beamValue); Serial.print(','); Serial.println(finishThreshold);
}

void beginRun() { state = RUNNING; runStartedAt = millis(); darkSampleCount = 0; Serial.println(F("EVENT,START")); }

void finishRun(int light) {
  unsigned long elapsed = millis() - runStartedAt;
  state = FINISHED; stateChangedAt = millis();
  Serial.print(F("EVENT,RESULT,")); Serial.print(elapsed); Serial.print(','); Serial.print(light); Serial.print(','); Serial.println(finishThreshold);
  if (dfPlayerReady) dfPlayer.play(2);
}

void setup() {
  pinMode(START_BUTTON_PIN, INPUT_PULLUP);
  Serial.begin(115200); loadConfig();
  dfPlayerSerial.begin(9600);
  if (dfPlayer.begin(dfPlayerSerial, true, true)) { dfPlayerReady = true; dfPlayer.volume(config.volume); Serial.println(F("EVENT,AUDIO_OK")); }
  else Serial.println(F("EVENT,AUDIO_NOT_FOUND"));
  calibrate(); sendConfig();
}

void loop() {
  processCommand();
  if (state == READY) { if (startButtonPressed()) beginStartSequence(); }
  else if (state == STARTING) { if (millis() - stateChangedAt >= config.startLeadMs) beginRun(); }
  else if (state == RUNNING) {
    if (millis() - runStartedAt >= config.finishArmMs) {
      int light = analogRead(LDR_PIN);
      if (light < finishThreshold) { if (++darkSampleCount >= config.darkSamples) finishRun(light); }
      else darkSampleCount = 0;
    }
  } else if (millis() - stateChangedAt >= config.resultHoldMs) { calibrate(); state = READY; }
}

“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik