To accurately measure the velocity of an RC car, slot car, or gravity racer, an Arduino speed detector built with dual infrared (IR) obstacle sensors provides sub-millisecond precision without the acoustic blind spots inherent to ultrasonic modules. By spacing two IR gates exactly 100 mm apart and utilizing hardware interrupts on an Arduino Nano v3, you can capture transit times down to 4-microsecond resolution, yielding reliable speed calculations for objects moving up to 60 mph (26.8 m/s).
Unlike polling-based sensor reads that can miss fast-moving objects between loop cycles, hardware interrupts freeze the microcontroller's current task to timestamp the exact microsecond a beam breaks. Below is the complete blueprint for building, coding, and debugging this timing gate on the workbench.
System Architecture and Component Selection
The most common mistake in DIY speed gates is selecting the wrong sensor technology. Ultrasonic sensors like the HC-SR04 are ubiquitous and cheap, but their 38-millisecond ping cycle and 30-degree beam width make them entirely unsuitable for high-speed transit detection. A 30 mph RC car travels 13.4 meters per second; it will completely pass through an ultrasonic blind spot before the sensor finishes a single ping.
For sub-millisecond timing, we use the FC-03 IR Obstacle Sensor. It pairs an infrared emitting diode with a phototransistor and an LM393 dual comparator, outputting a clean digital LOW when the reflected beam crosses the potentiometer-set threshold.
| Sensor Module | Core IC / Tech | Response Time | Beam Angle | Max Reliable Range | Approx Cost (2026) |
|---|---|---|---|---|---|
| FC-03 IR Obstacle | LM393 Comparator | ~10 µs | ~15° | 40 cm | $1.50 / pair |
| HC-SR04P Ultrasonic | Acoustic Ping/Echo | 38 ms (min) | ~30° | 400 cm | $2.00 |
| VL53L0X Laser ToF | Time-of-Flight (I2C) | 20 ms (cont.) | ~25° | 200 cm | $4.50 |
| A3144 Hall Effect | Magnetic Switch | ~2 µs | N/A (Magnetic) | 2 cm | $0.80 |
Note: Hall effect sensors are faster but require mounting a neodymium magnet to the vehicle, which isn't always practical for casual track days. The FC-03 offers the best balance of speed, non-contact operation, and cost.
Wiring the Dual-IR Sensor Gate
This build targets the Arduino Nano v3 (ATmega328P) due to its compact footprint, allowing the entire logic board to be mounted directly under the track. You will need the following exact components:
- Microcontroller: Arduino Nano v3 (ATmega328P, 16MHz)
- Sensors: 2x FC-03 IR Obstacle Avoidance Modules (LM393 version)
- Power: 5V 2A USB Power Supply (to ensure stable current for the IR LEDs)
- Passives: 2x 100nF (0.1µF) ceramic capacitors (critical for EMI filtering)
- Hardware: Breadboard or perfboard, 22 AWG solid copper wire, hot glue or M3 screws for mounting
Pin Mapping Table
| Arduino Nano Pin | Component | Function / Notes |
|---|---|---|
| D2 (INT0) | Sensor 1 OUT | Start Gate (Hardware Interrupt) |
| D3 (INT1) | Sensor 2 OUT | Stop Gate (Hardware Interrupt) |
| 5V | Sensor 1 & 2 VCC | Shared power rail |
| GND | Sensor 1 & 2 GND | Shared ground rail |
| D13 (LED) | Onboard LED | Visual transit indicator |
Physical Mounting Steps
- Establish the Baseline: Cut a wooden or 3D-printed baseplate. Draw a center line representing the track edge.
- Set Exact Spacing: Mount Sensor 1 and Sensor 2 exactly 100 mm (10 cm) apart, center-to-center of the IR LEDs. This exact distance simplifies the firmware math ($v = 0.1m / t$).
- Align the Optics: Point both sensors perpendicular to the track. Ensure the vehicle's chassis will pass through both beams at the same height.
- Tune the Potentiometers: Power the sensors. Use a small Phillips screwdriver to adjust the blue trimpot on each FC-03. Place your hand where the car will pass. Turn the pot until the onboard red LED just turns off, then back it off a quarter-turn. This sets the trigger threshold to roughly 5-10 cm.
Firmware: Calculating Velocity with Interrupts
The firmware relies on attachInterrupt() to capture the exact microsecond the beam breaks. Polling digitalRead() inside the loop() is forbidden here; a standard loop takes 4-10µs to execute, and at 40 mph, a car moves 1.7 mm per microsecond. Polling introduces unacceptable jitter.
The code below implements a simple state machine to ensure we only calculate speed when Sensor 1 triggers before Sensor 2, and includes a timeout handler to reset the system if a car stops halfway through the gate.
/*
* Arduino Speed Detector for RC Cars
* Target Board: Arduino Nano v3 (ATmega328P)
* Sensors: 2x FC-03 IR Obstacle (LM393)
* Distance between sensors: 0.100 meters (100mm)
*/
#define SENSOR1_PIN 2 // Hardware INT0
#define SENSOR2_PIN 3 // Hardware INT1
#define DISTANCE_M 0.100 // 100mm spacing
#define TIMEOUT_US 5000000 // 5-second timeout to prevent lockups
// Volatile variables modified by Interrupt Service Routines (ISRs)
volatile unsigned long start_time = 0;
volatile unsigned long end_time = 0;
volatile byte gate_state = 0; // 0=Idle, 1=S1 Broken, 2=Transit Complete
void setup() {
Serial.begin(115200);
while (!Serial) { ; } // Wait for serial port (Nano v3 USB)
pinMode(SENSOR1_PIN, INPUT_PULLUP);
pinMode(SENSOR2_PIN, INPUT_PULLUP);
pinMode(LED_BUILTIN, OUTPUT);
// Attach interrupts on FALLING edge (LM393 pulls LOW on detection)
attachInterrupt(digitalPinToInterrupt(SENSOR1_PIN), isr_sensor1, FALLING);
attachInterrupt(digitalPinToInterrupt(SENSOR2_PIN), isr_sensor2, FALLING);
Serial.println(F("--- Arduino Speed Detector Initialized ---"));
Serial.println(F("Waiting for transit..."));
}
void loop() {
// Handle completed transit
if (gate_state == 2) {
// Disable interrupts briefly to safely read multi-byte volatile vars
noInterrupts();
unsigned long t1 = start_time;
unsigned long t2 = end_time;
gate_state = 0; // Reset state machine
interrupts();
unsigned long transit_time_us = t2 - t1;
if (transit_time_us > 0) {
float transit_time_s = transit_time_us / 1000000.0;
float speed_mps = DISTANCE_M / transit_time_s;
float speed_mph = speed_mps * 2.23694;
Serial.print(F("Transit: "));
Serial.print(transit_time_us);
Serial.print(F(" us | Speed: "));
Serial.print(speed_mph, 2);
Serial.println(F(" mph"));
// Blink LED to confirm capture
digitalWrite(LED_BUILTIN, HIGH);
delay(100);
digitalWrite(LED_BUILTIN, LOW);
}
}
// Handle timeout (car stopped between sensors or missed S2)
if (gate_state == 1) {
noInterrupts();
unsigned long t1 = start_time;
interrupts();
if (micros() - t1 > TIMEOUT_US) {
Serial.println(F("Speed: 0.00 m/s | Err: Timeout - Object stalled in gate"));
gate_state = 0; // Reset
}
}
}
// ISR for Start Gate
void isr_sensor1() {
if (gate_state == 0) {
start_time = micros();
gate_state = 1;
}
}
// ISR for Stop Gate
void isr_sensor2() {
if (gate_state == 1) {
end_time = micros();
gate_state = 2;
}
}
Bench Testing and Calibration
Before taking the gate to the track, validate the timing on your bench. Open the Arduino IDE Serial Monitor at 115200 baud. Drop a pen or roll a ball through the sensors. You should see transit times in the 20,000 to 50,000 microsecond range (20-50 ms), translating to roughly 2 to 5 m/s.
#define DISTANCE_M 0.098 macro in the code. Do not attempt to "fudge" the math in the loop; always correct the physical constant.
Troubleshooting: "Speed Reads 0.00" and Phantom Triggers
When deploying embedded timing systems in noisy environments (like near brushed DC motors or 2.4GHz RC transmitters), you will inevitably encounter edge cases. Here is the diagnostic decision tree for the two most common failure modes.
Error 1: Speed: 0.00 m/s | Err: Timeout - Object stalled in gate
This error string prints when Sensor 1 triggers, but Sensor 2 fails to trigger within the 5-second window. Ranked causes:
- Sensor 2 Potentiometer Misalignment: The threshold on Sensor 2 is set too high, meaning the object passes without breaking the beam. Fix: Re-tune the trimpot on Sensor 2 while passing the object manually.
- Object Too Slow / Stalled: The RC car literally ran out of battery or crashed between the gates. Fix: Clear the track; the code automatically resets after printing the error.
- Broken Jumper Wire on D3: The signal wire from Sensor 2 to Pin D3 has high resistance or is disconnected. Fix: Use a multimeter in continuity mode to verify the trace from the sensor OUT pin to the Nano D3 header.
Error 2: Speed: 999.99 mph (or erratic massive numbers)
This happens when the transit time calculates to just a few microseconds, which is physically impossible for a 10cm gap. This is a Phantom Trigger. Ranked causes:
- EMI / RFI Coupling: The RC car's motor brushes are emitting RF noise that induces a voltage spike in the high-impedance sensor wires, tricking the ATmega328P into firing the interrupt. Fix: Verify the 100nF decoupling capacitors are soldered directly to the sensor VCC/GND pins. If using long wires, switch to shielded cable and tie the shield to GND at the Nano end only.
- IR Reflection Bounce: Sunlight or a glossy track surface is bouncing ambient IR into the receiver, causing the LM393 to chatter. Fix: Build a small cardboard or 3D-printed shroud (hood) over the phototransistor to block ambient light, and lower the sensor height to 2 cm above the matte track surface.
- Missing Internal Pull-ups: The FC-03 output is open-collector. While the code specifies
INPUT_PULLUP, if you accidentally wired it asINPUT, the line will float. Fix: Ensure the code usesINPUT_PULLUPor add external 10kΩ pull-up resistors to 5V.
First Three Things to Check When the System Fails Completely
If the Serial Monitor is entirely blank or the system refuses to register any passes, check these three items immediately:
- Baud Rate Mismatch: Ensure your Serial Monitor is set to 115200 baud. A 9600 baud setting will output garbage characters, making it look like the system is dead.
- Sensor Power LEDs: Look at the FC-03 boards. The green power LED must be lit. If it's off, your Nano's 5V rail is browned out or the USB cable is charge-only (missing data lines, preventing Serial handshake).
- Interrupt Pin Mapping: Verify you are using D2 and D3. On the ATmega328P, these are the only pins that support hardware interrupts (INT0 and INT1). Wiring to D4 or D5 will result in silent failure.
How to Extend or Simplify the Build
To Simplify: If you are measuring slow objects (like a crawling robot or a pedestrian) and don't care about microsecond precision, you can strip out the interrupts entirely. Replace the ISRs with pulseIn(SENSOR1_PIN, LOW) and standard polling. However, be aware that pulseIn() is a blocking function and will freeze the microcontroller while waiting for the second sensor.
To Extend:
For track-day logging, add an I2C OLED display (SSD1306, 128x64) using the Adafruit_SSD1306 library to show the last 5 run times. For bidirectional timing (capturing cars moving left-to-right and right-to-left), expand the state machine to track which sensor triggers first, using a negative sign to denote reverse direction. You can also integrate an ESP32-C3 in place of the Nano to push lap times via MQTT to a local Raspberry Pi server for real-time leaderboard displays.
For deeper reading on hardware interrupt behavior and timing limits on AVR microcontrollers, refer to the official Arduino attachInterrupt() documentation. For electrical characteristics of the comparator used in the FC-03, consult the Texas Instruments LM393 Datasheet.






