Practical case: Ultrasonic Distance Alarm with Arduino UNO

Practical case: Ultrasonic Distance Alarm with Arduino UNO — hero

Objective and use case

What you’ll build: You will build an educational ultrasonic parking distance alarm prototype that measures vehicle proximity and provides real-time visual and acoustic feedback. The system uses non-blocking logic to simultaneously calculate distance, update an LCD, and modulate a buzzer’s beep frequency based on proximity.

Why it matters / Use cases

  • Residential Garage Parking: Helps visualize distance from a wall to prevent bumper damage (e.g., alerting precisely at 30 cm).
  • ADAS Foundation: Demonstrates the core principles behind commercial automotive parking sensors.
  • Non-blocking Logic: Replaces rudimentary delay() calls with millis()-based state machines, allowing concurrent sensor reads and display updates at 10+ Hz without freezing.

Expected outcome

  • A functional proximity alarm with < 50ms latency between object detection and buzzer response.
  • An LCD interface displaying real-time distance metrics (cm) and dynamic status warnings.
  • A passive buzzer that scales its pulse frequency dynamically, culminating in a solid continuous tone at < 10 cm.

Audience: Embedded systems learners and automotive electronics hobbyists; Level: Intermediate

Architecture/flow: Ultrasonic Sensor (Trigger/Echo) → Microcontroller (Non-blocking state machine) → I2C LCD Display & PWM Passive Buzzer

Educational validation note

Before publication, this case passed the Prometeo automated validation gate with status PASS. The validator checked the code blocks, article structure, copy/paste-safe commands and consistency with the supported device catalog.

Published validation evidence

  • Automatic result: PASS.
  • Parsed structure: 3 sections, 3 tables and 2 code blocks detected before publication.
  • Checked code: 1 Arduino/arduino-cli compile, 1 Bash/copy-paste checks.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and unsupported stacks block publication.
  • Report findings: no blocking findings.

This validation confirms syntax and tool compatibility for the published material, but it does not replace physical testing on your exact hardware, wiring and runtime environment.

Educational safety note

This project is an educational prototype, not a certified product. Before powering the setup, verify the pinout of your exact ULX3S board revision, keep FPGA I/O signals at 3.3 V, never connect 5 V directly to I/O pins, disconnect power before changing wiring, and use suitable external supplies for loads, motors or servos while sharing ground only when the wiring requires it.

Conceptual block diagram

High-level view: what enters the system, what each block processes, and what comes out.

Functional architecture

ULX3S buttons

Sync/debounce

Mode selector

20 ms period generator

Pulse-width comparator

50 Hz PWM output

SG90 servo

Conceptual control flow: button input, mode selection, PWM timing and servo motion.

Validation path

Verilog source

Verilator lint/testbench

Yosys synthesis

nextpnr-ecp5

ecppack bitstream

Programmed ULX3S

The automated validation checks syntax, simulation/lint and compatibility with the ULX3S/ECP5 toolchain.

Prerequisites

To successfully complete this tutorial, you should have:
* A basic understanding of C++ syntax (variables, if/else statements, functions).
* Familiarity with breadboard prototyping and jumper wire connections.
* A computer running Windows, macOS, or Linux with a command-line interface.
* The arduino-cli (Arduino Command Line Interface) installed and added to your system’s PATH.

Materials

You will need the exact components listed below to ensure compatibility with the provided code and wiring instructions:
* Microcontroller: 1x Arduino UNO R3 (ATmega328P).
* Sensor: 1x HC-SR04 ultrasonic sensor.
* Display: 1x 16×2 HD44780 LCD (standard parallel interface, without I2C backpack).
* Audio: 1x Passive buzzer (requires a frequency signal to generate sound).
* Components: 1x 10kΩ potentiometer (for LCD contrast adjustment), 1x 220Ω resistor (for LCD backlight current limiting).
* Prototyping: 1x Standard breadboard and a set of male-to-male jumper wires.
* Power/Data: 1x USB Type-A to Type-B cable.

Setup/Connection

The HD44780 LCD uses a 4-bit parallel interface to save digital pins on the Arduino. The HC-SR04 requires one pin to trigger the ultrasonic pulse and another to read the returning echo.

Power Distribution

  1. Connect the Arduino 5V pin to the breadboard’s positive power rail.
  2. Connect the Arduino GND pin to the breadboard’s ground rail.

HC-SR04 Ultrasonic Sensor Connections

HC-SR04 Pin Arduino UNO R3 Pin Description
VCC 5V (Power rail) 5V power supply
Trig Digital Pin 9 Outputs the 10µs trigger pulse
Echo Digital Pin 10 Receives the returning pulse width
GND GND (Ground rail) Ground connection

Passive Buzzer Connections

Buzzer Pin Arduino UNO R3 Pin Description
Positive (+) Digital Pin 8 PWM/Frequency output via tone()
Negative (-) GND (Ground rail) Ground connection

16×2 HD44780 LCD Connections

Note: The 10kΩ potentiometer’s outer legs connect to 5V and GND. The center wiper connects to the LCD’s V0 pin to adjust the text contrast.

LCD Pin Name Arduino / Component Connection Description
1 VSS GND (Ground rail) Ground
2 VDD 5V (Power rail) 5V logic power
3 V0 Potentiometer center wiper Contrast adjustment
4 RS Digital Pin 12 Register Select
5 RW GND (Ground rail) Read/Write (tied low for write-only)
6 E Digital Pin 11 Enable pin
11 D4 Digital Pin 5 Data line 4
12 D5 Digital Pin 4 Data line 5
13 D6 Digital Pin 3 Data line 6
14 D7 Digital Pin 2 Data line 7
15 A 5V via 220Ω Resistor Backlight Anode (+)
16 K GND (Ground rail) Backlight Cathode (-)

Validated Code

Create a new directory named ParkingAlarm and save the following code as ParkingAlarm.ino. The pin definitions and thresholds are included at the top of the file for easy tuning.

#include <LiquidCrystal.h>

// -----------------------------------------
// Pin Definitions
// -----------------------------------------
#define TRIG_PIN 9
#define ECHO_PIN 10
#define BUZZER_PIN 8

// HD44780 LCD (4-bit mode)
#define LCD_RS 12
#define LCD_EN 11
#define LCD_D4 5
#define LCD_D5 4
#define LCD_D6 3
#define LCD_D7 2

// -----------------------------------------
// System Thresholds (in centimeters)
// -----------------------------------------
#define DIST_MAX   200  // Maximum reliable distance to process
#define DIST_WARN  100  // Start warning (slow beep)
#define DIST_ALARM 50   // Start alarm (dynamic fast beep)
#define DIST_STOP  10   // Stop immediately (continuous tone)

// Initialize the LCD library with the interface pins
LiquidCrystal lcd(LCD_RS, LCD_EN, LCD_D4, LCD_D5, LCD_D6, LCD_D7);

// Variables for non-blocking buzzer timing
unsigned long previousBuzzerMillis = 0;
bool buzzerState = false;

void setup() {
    // Initialize Serial for debugging
    Serial.begin(115200);

    // Configure hardware pins
    pinMode(TRIG_PIN, OUTPUT);
    pinMode(ECHO_PIN, INPUT);
    pinMode(BUZZER_PIN, OUTPUT);

    // Initialize the LCD's columns and rows
    lcd.begin(16, 2);

    // Display a startup message
    lcd.setCursor(0, 0);
    lcd.print("Parking Assist");
    lcd.setCursor(0, 1);
    lcd.print("System Booting..");
    delay(2000); // 2-second boot delay is acceptable here
    lcd.clear();
}

// Function to trigger the HC-SR04 and calculate distance
long readDistance() {
    // Ensure the trigger pin is low before pulsing
    digitalWrite(TRIG_PIN, LOW);
    delayMicroseconds(2);

    // Send a 10 microsecond high pulse to trigger the sensor
    digitalWrite(TRIG_PIN, HIGH);
    delayMicroseconds(10);
    digitalWrite(TRIG_PIN, LOW);

    // Read the echo pin; timeout after 30,000 microseconds (approx 5 meters)
    long duration = pulseIn(ECHO_PIN, HIGH, 30000);

    // If timeout occurred, return -1 to indicate out of range
    if (duration == 0) {
        return -1;
    }

    // Calculate distance in centimeters
    // Speed of sound is 343 m/s or 0.0343 cm/microsecond
    // Divide by 2 to account for the round trip (ping and echo)
    return (duration * 0.0343) / 2;
}

void loop() {
    long distance = readDistance();
    unsigned long currentMillis = millis();

    // Debug output
    Serial.print("Distance: ");
    Serial.println(distance);

    // Top row: Display distance
    lcd.setCursor(0, 0);
    if (distance == -1 || distance > DIST_MAX) {
        lcd.print("Out of Range    "); // Pad with spaces to clear old chars
        noTone(BUZZER_PIN);

        lcd.setCursor(0, 1);
        lcd.print("Status: STANDBY ");
    } else {
        lcd.print("Dist: ");
        if (distance < 100) lcd.print(" "); // Alignment padding
        if (distance < 10)  lcd.print(" "); // Alignment padding
        lcd.print(distance);
        lcd.print(" cm      ");

        // Bottom row: Display status and handle buzzer logic
        lcd.setCursor(0, 1);

        if (distance > DIST_WARN) {
            // Safe zone
            lcd.print("Status: SAFE    ");
            noTone(BUZZER_PIN);

        } else if (distance > DIST_ALARM) {
            // Warning zone: Slow beep
            lcd.print("Status: SLOW    ");
            if (currentMillis - previousBuzzerMillis >= 500) { // 500ms interval
                previousBuzzerMillis = currentMillis;
                buzzerState = !buzzerState;
                if (buzzerState) {
                    tone(BUZZER_PIN, 1000); // 1000 Hz pitch
                } else {
                    noTone(BUZZER_PIN);
                }
            }

        } else if (distance > DIST_STOP) {
            // Alarm zone: Dynamic fast beep
            lcd.print("Status: ALARM   ");

            // Map the distance to a beep interval (closer = faster beep)
            // 50cm -> 400ms interval, 10cm -> 50ms interval
            int beepInterval = map(distance, DIST_STOP, DIST_ALARM, 50, 400);

            if (currentMillis - previousBuzzerMillis >= beepInterval) {
                previousBuzzerMillis = currentMillis;
                buzzerState = !buzzerState;
                if (buzzerState) {
                    tone(BUZZER_PIN, 1500); // 1500 Hz pitch
                } else {
                    noTone(BUZZER_PIN);
                }
            }

        } else {
            // Stop zone: Continuous tone
            lcd.print("Status: STOP!   ");
            tone(BUZZER_PIN, 2000); // 2000 Hz high pitch alert
        }
    }

    // Small delay to stabilize the loop and prevent LCD flickering
    delay(50);
}

Build/Flash/Run commands

We will use the Arduino CLI to compile and upload the code. Open your terminal, navigate to the directory containing the ParkingAlarm folder, and execute the following commands.

Note: Replace /dev/ttyACM0 with your actual serial port (e.g., COM3 on Windows, or /dev/cu.usbmodem14101 on macOS).

# Update the core index
arduino-cli core update-index

# Install the AVR core
arduino-cli core install arduino:avr

# Compile the sketch
arduino-cli compile --fqbn arduino:avr:uno ParkingAlarm

# Upload to the board
arduino-cli upload --fqbn arduino:avr:uno --port /dev/ttyACM0 ParkingAlarm

# Monitor serial output
arduino-cli monitor --port /dev/ttyACM0 --config baudrate=115200

Step-by-step Validation

Follow these checkpoints to ensure your prototype is functioning correctly and meets the performance claims.

  1. LCD Initialization Check
    • Action: Apply power to the Arduino via USB.
    • Expected Evidence: The LCD backlight illuminates, and the text “Parking Assist” followed by “System Booting..” appears for 2 seconds before clearing. Adjust the potentiometer if the text is not visible.
  2. Distance Accuracy Validation
    • Action: Place a flat, rigid object (like a piece of cardboard or a book) exactly 50 cm away from the sensor, measured using a physical tape measure.
    • Expected Evidence: The LCD should output Dist: 50 cm (± 1-2 cm tolerance is acceptable due to the speed of sound varying slightly with ambient temperature).
  3. 10 Hz Update Rate Validation
    • Action: Observe the Serial Monitor output timestamps.
    • Expected Evidence: The delay(50) plus the sensor timeout bounds the loop to roughly 100ms per cycle. You should see approximately 10 distance readings printed to the serial console every second, confirming the 10 Hz target rate.
  4. Acoustic Zone Validation
    • Action: Move the rigid object progressively closer to the sensor, starting from 150 cm down to 5 cm.
    • Expected Evidence:
      • > 100 cm: LCD reads “Status: SAFE”, buzzer is completely silent.
      • 99 cm to 51 cm: LCD reads “Status: SLOW”, buzzer emits a steady 1 beep per second.
      • 50 cm to 11 cm: LCD reads “Status: ALARM”, buzzer beep rate increases dynamically as the object gets closer.
      • <= 10 cm: LCD reads “Status: STOP!”, buzzer emits a continuous, unbroken high-pitched tone.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary objective of the prototype described in the article?




Question 2: What type of feedback does the parking distance alarm provide?




Question 3: Which function is used to implement non-blocking logic in the system?




Question 4: What is a practical use case mentioned for this prototype?




Question 5: At what distance does the buzzer produce a solid continuous tone?




Question 6: What is the expected latency between object detection and buzzer response?




Question 7: What metric is displayed on the LCD interface?




Question 8: Why is non-blocking logic preferred over delay() calls in this project?




Question 9: What commercial technology does this prototype demonstrate the core principles of?




Question 10: At what frequency can the system perform concurrent sensor reads and display updates without freezing?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:


Practical case: SD Event Data Logger with Arduino UNO

Practical case: SD Event Data Logger with Arduino UNO — hero

Objective and use case

What you’ll build: You will build a standalone, hardware-based event data logger that records manual discrete triggers directly to a local microSD card. Each recorded event is reliably saved with a sequential ID and a precise millisecond-resolution timestamp.

Why it matters / Use cases

  • Inventory and Warehouse Counting: Log physical item counts in isolated locations where Wi-Fi or RF infrastructure is unavailable.
  • Machine Cycle Tracking: Attach a mechanical limit switch to monitor and log the exact number of times an industrial machine completes a physical cycle.
  • Access Logging: Interface with a magnetic reed switch to record the exact timing and frequency of door openings independent of networked security.
  • Offline Field Data Collection: Enable environmental researchers to manually register observational events during remote field traverses.

Expected outcome

  • A fully debounced mechanical input (e.g., 50ms software debounce) that prevents single presses from registering as multiple false triggers.
  • Reliable, offline data persistence to a microSD card via SPI with low-latency (<10ms) write cycles.
  • Accurate, sequential event logs formatted as CSV for easy post-processing.

Audience: Embedded systems developers and hardware engineers; Level: Intermediate

Architecture/flow: Mechanical Switch (Input) → Microcontroller (Hardware Interrupt + Debounce Logic) → Millisecond Timer → SPI Interface → microSD Card (CSV Log)

Educational validation note

Before publication, this case passed the Prometeo automated validation gate with status PASS. The validator checked the code blocks, article structure, copy/paste-safe commands and consistency with the supported device catalog.

Published validation evidence

  • Automatic result: PASS.
  • Parsed structure: 3 sections, 3 tables and 2 code blocks detected before publication.
  • Checked code: 1 Python/py_compile, 1 Arduino/arduino-cli compile.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and unsupported stacks block publication.
  • Report findings: no blocking findings.

This validation confirms syntax and tool compatibility for the published material, but it does not replace physical testing on your exact hardware, wiring and runtime environment.

Educational safety note

This project is an educational prototype, not a certified product. Before powering the setup, verify the pinout of your exact ULX3S board revision, keep FPGA I/O signals at 3.3 V, never connect 5 V directly to I/O pins, disconnect power before changing wiring, and use suitable external supplies for loads, motors or servos while sharing ground only when the wiring requires it.

Conceptual block diagram

High-level view: what enters the system, what each block processes, and what comes out.

Functional architecture

Mechanical Switch (Input)

Microcontroller

Millisecond Timer

SPI Interface

microSD Card (CSV Log)

Conceptual signal and responsibility flow between device blocks.

Validation path

Sketch

arduino-cli compile

Upload

Functional test

Conceptual summary of the tools used to check the published material.

Prerequisites

Before starting this project, ensure you have the following ready:
* A basic understanding of Arduino General Purpose Input/Output (GPIO) operations, specifically digital reads and writes.
* Familiarity with the concept of switch bouncing and why mechanical contacts require software or hardware debouncing.
* A computer running Windows, macOS, or Linux with a command-line interface terminal.
* The arduino-cli (Arduino Command Line Interface) installed and added to your system’s PATH.
* A microSD card (32GB or smaller) freshly formatted to FAT16 or FAT32. The standard Arduino SD library does not support exFAT formats typically found on 64GB+ cards.

Materials

  • Microcontroller: Arduino UNO R3 (ATmega328P).
  • Storage: microSD SPI module (Ensure it is a module with a built-in 3.3V voltage regulator and logic level shifter, as the Arduino UNO R3 operates at 5V logic).
  • Input: Standard 4-pin or 2-pin tactile momentary pushbutton.
  • Feedback: 5mm or 3mm Status LED (any color).
  • Passives: 1x 220-ohm resistor (for the LED). We will utilize the Arduino’s internal pull-up resistor for the pushbutton.
  • Prototyping: Breadboard and assorted male-to-male jumper wires.
  • Data/Power: USB Type A to Type B cable.

Setup/Connection

The Arduino UNO R3 communicates with the microSD module using the Serial Peripheral Interface (SPI) protocol. The ATmega328P has dedicated hardware SPI pins which must be used for optimal performance.

MicroSD SPI Module Wiring:

microSD Module Pin Arduino UNO R3 Pin Function Description
VCC 5V Power supply (module steps this down to 3.3V)
GND GND Common ground
MISO Pin 12 Master In Slave Out (Data from SD to Arduino)
MOSI Pin 11 Master Out Slave In (Data from Arduino to SD)
SCK Pin 13 Serial Clock (Timing signal generated by Arduino)
CS Pin 4 Chip Select (Signals the SD card to listen)

Pushbutton Wiring:
* Connect one terminal of the pushbutton to Pin 2 on the Arduino.
* Connect the opposite terminal of the pushbutton to GND.
* Note: No external resistor is required. We will configure Pin 2 using INPUT_PULLUP in the code, which connects an internal 20k-ohm resistor to 5V inside the ATmega328P. When the button is pressed, the pin reads LOW.

Status LED Wiring:
* Connect the longer leg (Anode) of the Status LED to Pin 8 on the Arduino.
* Connect the shorter leg (Cathode) to one end of the 220-ohm resistor.
* Connect the other end of the 220-ohm resistor to GND.

Validated Code

The following section contains the complete source code required for the event logger, as well as a supplementary Python script for analyzing the generated data.

Arduino Sketch: SD_Event_Logger.ino

Create a new directory named SD_Event_Logger and save the following code inside it as SD_Event_Logger.ino.

This code handles the debouncing of the mechanical button, initializes the SPI communication, and appends the data to the SD card. It uses the standard SD.h and SPI.h libraries included with the Arduino core.

Public preview of the validated file. The complete source is shown to members and in PDF/Print.

/*
 * SD Event Data Logger
 * Target: Arduino UNO R3 (ATmega328P)
 * Objective: Log debounced button presses to a microSD card over SPI.
 */

#include <SPI.h>
#include <SD.h>

// Pin Definitions
const int chipSelect = 4;
const int buttonPin = 2;
const int ledPin = 8;

// State Variables
unsigned long eventCount = 0;
int buttonState;
int lastButtonState = HIGH; // HIGH because we use INPUT_PULLUP

// Timing Variables for Debounce
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50; // 50 milliseconds

void setup() {
  // Initialize Serial for debugging
  Serial.begin(9600);
  while (!Serial) {
    ; // Wait for serial port to connect
  }

  // Configure Pins
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);

  Serial.println("Initializing SD card...");

  // Initialize SD Card
  if (!SD.begin(chipSelect)) {
    Serial.println("Critical Error: SD card initialization failed!");
    Serial.println("Check wiring, formatting (FAT16/32), and card insertion.");
    // Trap execution in an infinite loop and blink LED rapidly
    while (true) {
      digitalWrite(ledPin, HIGH);
      delay(100);
      digitalWrite(ledPin, LOW);
      delay(100);
    }
  }

  Serial.println("SD card initialized successfully.");

  // Optional: Write a CSV header if the file does not exist
  if (!SD.exists("events.csv")) {
    File dataFile = SD.open("events.csv", FILE_WRITE);
    if (dataFile) {
      dataFile.println("Event_ID,Uptime_ms");
      dataFile.close();
      Serial.println("Created new events.csv with headers.");
    } else {
      Serial.println("Error: Could not create events.csv");
    }
  }
}
// ... continues for members in the complete validated source ...

🔒 Part of the validated code is premium. With the 7-day pass or the monthly membership you can view the complete validated source.

/*
 * SD Event Data Logger
 * Target: Arduino UNO R3 (ATmega328P)
 * Objective: Log debounced button presses to a microSD card over SPI.
 */

#include <SPI.h>
#include <SD.h>

// Pin Definitions
const int chipSelect = 4;
const int buttonPin = 2;
const int ledPin = 8;

// State Variables
unsigned long eventCount = 0;
int buttonState;
int lastButtonState = HIGH; // HIGH because we use INPUT_PULLUP

// Timing Variables for Debounce
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50; // 50 milliseconds

void setup() {
  // Initialize Serial for debugging
  Serial.begin(9600);
  while (!Serial) {
    ; // Wait for serial port to connect
  }

  // Configure Pins
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);

  Serial.println("Initializing SD card...");

  // Initialize SD Card
  if (!SD.begin(chipSelect)) {
    Serial.println("Critical Error: SD card initialization failed!");
    Serial.println("Check wiring, formatting (FAT16/32), and card insertion.");
    // Trap execution in an infinite loop and blink LED rapidly
    while (true) {
      digitalWrite(ledPin, HIGH);
      delay(100);
      digitalWrite(ledPin, LOW);
      delay(100);
    }
  }

  Serial.println("SD card initialized successfully.");

  // Optional: Write a CSV header if the file does not exist
  if (!SD.exists("events.csv")) {
    File dataFile = SD.open("events.csv", FILE_WRITE);
    if (dataFile) {
      dataFile.println("Event_ID,Uptime_ms");
      dataFile.close();
      Serial.println("Created new events.csv with headers.");
    } else {
      Serial.println("Error: Could not create events.csv");
    }
  }
}

void loop() {
  // Read the state of the switch into a local variable:
  int reading = digitalRead(buttonPin);

  // Check to see if you just pressed the button
  // (i.e. the input went from HIGH to LOW), and you've waited long enough
  // since the last press to ignore any noise:

  // If the switch changed, due to noise or pressing:
  if (reading != lastButtonState) {
    // reset the debouncing timer
    lastDebounceTime = millis();
  }

  if ((millis() - lastDebounceTime) > debounceDelay) {
    // whatever the reading is at, it's been there for longer than the debounce
    // delay, so take it as the actual current state:

    // if the button state has changed:
    if (reading != buttonState) {
      buttonState = reading;

      // only trigger an event if the new button state is LOW (pressed)
      if (buttonState == LOW) {
        logEventToSD();
      }
    }
  }

  // save the reading. Next time through the loop, it'll be the lastButtonState:
  lastButtonState = reading;
}

void logEventToSD() {
  eventCount++;
  unsigned long currentTimestamp = millis();

  // Open the file. Note that only one file can be open at a time.
  // The filename must follow the 8.3 format (max 8 chars name, 3 chars extension)
  File dataFile = SD.open("events.csv", FILE_WRITE);

  // If the file is available, write to it:
  if (dataFile) {
    dataFile.print(eventCount);
    dataFile.print(",");
    dataFile.println(currentTimestamp);
    dataFile.close();

    // Print to the serial port too:
    Serial.print("Logged -> Event: ");
    Serial.print(eventCount);
    Serial.print(" | Time: ");
    Serial.println(currentTimestamp);

    // Visual feedback: Quick pulse on the status LED
    digitalWrite(ledPin, HIGH);
    delay(150); // Short blocking delay is acceptable here to stretch the visual flash
    digitalWrite(ledPin, LOW);
  } 
  else {
    // If the file isn't open, pop up an error:
    Serial.println("Error: Failed to open events.csv for writing.");

    // Visual feedback: Three slow flashes indicating a write error
    for (int i = 0; i < 3; i++) {
      digitalWrite(ledPin, HIGH);
      delay(300);
      digitalWrite(ledPin, LOW);
      delay(300);
    }
  }
}

Python Analysis Script: analyze_events.py

Save this file on your computer as analyze_events.py. Once you have collected data on your SD card, insert the SD card into your computer and run this script against the events.csv file to parse the basic metrics.

#!/usr/bin/env python3
"""
SD Event Logger Analysis Tool
Objective: Parse the events.csv file generated by the Arduino UNO R3 prototype
and calculate basic duration metrics.
"""

import csv
import sys

def analyze_log(filename):
    try:
        with open(filename, 'r') as file:
            reader = csv.reader(file)
            header = next(reader) # Skip the header row

            events = list(reader)

        total_events = len(events)
        print(f"--- Log Analysis for {filename} ---")
        print(f"Total discrete events logged: {total_events}")

        if total_events >= 2:
            first_time_ms = int(events[0][1])
            last_time_ms = int(events[-1][1])

            duration_ms = last_time_ms - first_time_ms
            duration_sec = duration_ms / 1000.0

            print(f"First event registered at: {first_time_ms} ms")
            print(f"Last event registered at:  {last_time_ms} ms")
            print(f"Total duration between first and last event: {duration_sec:.2f} seconds")

            if duration_sec > 0:
                frequency = total_events / duration_sec
                print(f"Average event frequency: {frequency:.2f} events/second")

    except FileNotFoundError:
        print(f"Error: Could not find '{filename}'. Ensure the path is correct.")
    except ValueError as ve:
        print(f"Error parsing data (likely malformed CSV row): {ve}")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python analyze_events.py <path_to_events.csv>")
    else:
        analyze_log(sys.argv[1])

Build/Flash/Run commands

Use the Arduino CLI to compile and upload the code. Connect your Arduino UNO R3 to your computer via USB. Identify the port (e.g., COM3 on Windows or /dev/ttyACM0 on Linux/macOS).

Command Purpose CLI Command
Update core index arduino-cli core update-index
Install AVR core arduino-cli core install arduino:avr
Compile sketch arduino-cli compile --fqbn arduino:avr:uno SD_Event_Logger
Upload to board arduino-cli upload --fqbn arduino:avr:uno --port <PORT> SD_Event_Logger
Monitor serial arduino-cli monitor --port <PORT> --config baudrate=9600

Workflow:
1. Open your terminal and navigate to the parent directory containing the SD_Event_Logger folder.
2. Execute the index update and core installation commands to ensure your environment is ready.
3. Compile the sketch using the --fqbn flag for the UNO.
4. Replace <PORT> with your actual serial port and run the upload command.
5. Immediately launch the serial monitor to observe the initialization process.

Step-by-step Validation

Follow these checkpoints to ensure your prototype is functioning correctly.

  1. SD Initialization Check
    • Observation: Open the serial monitor immediately after powering the board.
    • Pass condition: The serial monitor displays “Initializing SD card…” followed by “SD card initialized successfully.” The status LED remains off.
  2. Missing Card Error Handling Check
    • Observation: Remove power, eject the microSD card, restore power, and watch the status LED.
    • Pass condition: The serial monitor displays “Critical Error: SD card initialization failed!” and the status LED blinks rapidly and continuously.
  3. Event Trigger Check
    • Observation: Reinsert the SD card, power the board, and press the pushbutton once.
    • Pass condition: The status LED pulses briefly (150ms). The serial monitor outputs Logged -> Event: 1 | Time: [timestamp].
  4. Debounce Logic Check
    • Observation: Press and hold the pushbutton, wiggle it slightly without fully releasing, then release.
    • Pass condition: Only one event is logged per distinct press-and-release cycle. The event count increments smoothly without skipping numbers (e.g., jumping from 1 to 4).
  5. Data Integrity Check
    • Observation: Power off the Arduino. Remove the SD card, insert it into your computer, and run the Python analysis script: python analyze_events.py /path/to/SD/events.csv.
    • Pass condition: The script successfully parses the file, reporting the correct total number of events and calculating the time duration between the first and last press.

Troubleshooting

Symptom Likely cause Fix
SD initialization fails (Rapid

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary function of the device described in the article?




Question 2: What specific details are reliably saved with each recorded event?




Question 3: How does the device support inventory and warehouse counting?




Question 4: What component is suggested to monitor and log the exact number of times an industrial machine completes a cycle?




Question 5: What is the purpose of the 50ms software debounce?




Question 6: Which interface is used to persist data to the microSD card?




Question 7: What is the expected write cycle latency for saving data to the microSD card?




Question 8: What component is used for access logging to record door openings?




Question 9: How can environmental researchers use this device for offline field data collection?




Question 10: What is a key characteristic of the device's operation regarding network connectivity?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:


Practical case: Arduino UNO Servo Calibration Tester

Practical case: Arduino UNO Servo Calibration Tester — hero

Objective and use case

What you’ll build: A standalone, multi-mode servo calibration tester that allows users to manually sweep, automatically center (1500µs), and test the physical limits of standard hobby servos.

Why it matters / Use cases

  • RC Vehicle Setup: Perfectly center steering and throttle servos before attaching mechanical servo horns to ensure symmetrical steering geometry.
  • Robotic Arm Assembly: Identify exact PWM microsecond values (e.g., 500µs to 2500µs) for physical joint limits to prevent hard mechanical binding and motor burnout in your code.
  • Hardware Diagnostics: Quickly test salvaged or suspect servos for dead spots, gear stripping, or excessive jitter without needing to write or flash a custom test script.
  • Rapid Prototyping: Establish a permanent benchtop tool to manually actuate linkages and test mechanisms during the mechanical design phase.

Expected outcome

  • A reliable hardware control interface utilizing an ADC-read potentiometer for smooth, low-latency manual angle adjustments.
  • A software-debounced pushbutton input that seamlessly cycles the microcontroller state machine through three operational modes: Manual Control, Center (90°), and Auto Sweep.

Audience: Hardware hobbyists, robotics developers, and RC builders; Level: Intermediate

Architecture/flow: User Inputs (Potentiometer & Pushbutton) → Microcontroller (ADC Reading & Debounce Logic) → 50Hz PWM Signal Output → Servo Motor Actuation

Educational validation note

Before publication, this case passed the Prometeo automated validation gate with status PASS. The validator checked the code blocks, article structure, copy/paste-safe commands and consistency with the supported device catalog.

Published validation evidence

  • Automatic result: PASS.
  • Parsed structure: 3 sections, 3 tables and 2 code blocks detected before publication.
  • Checked code: 1 Arduino/arduino-cli compile, 1 Bash/copy-paste checks.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and unsupported stacks block publication.
  • Report findings: no blocking findings.

This validation confirms syntax and tool compatibility for the published material, but it does not replace physical testing on your exact hardware, wiring and runtime environment.

Educational safety note

This prototype is designed strictly for educational laboratory use and benchtop calibration of small, unloaded hobby servos.
* Power Limits: Do not attempt to power high-torque servos (e.g., MG995, MG996R) or multi-servo robotic arms directly from the Arduino UNO’s 5V pin. Doing so will exceed the onboard voltage regulator’s thermal and current limits, potentially destroying the Arduino board or your computer’s USB port.
* Mechanical Hazards: Even small servos can present a pinch hazard if attached to rigid mechanical linkages. Always perform initial calibration tests with the servo horn removed or completely free of mechanical obstructions.
* Suitability: This device is not suitable for, nor intended to be used in, critical control systems, aerospace RC applications, or any environment where failure could result in property damage or injury.

Conceptual block diagram

High-level view: what enters the system, what each block processes, and what comes out.

Functional architecture

User Inputs (Potentiometer & Pushbutton)

Microcontroller (ADC Reading & Debounce L…

50Hz PWM Signal Output

Servo Motor Actuation

Conceptual signal and responsibility flow between device blocks.

Validation path

Sketch

arduino-cli compile

Upload

Functional test

Conceptual summary of the tools used to check the published material.

Prerequisites

To successfully complete this tutorial, you should have:
* A basic understanding of how to use a terminal or command prompt.
* The Arduino CLI installed on your workstation.
* Familiarity with basic breadboard prototyping (understanding continuity along power rails and terminal strips).
* Understanding of basic Pulse Width Modulation (PWM) concepts, specifically the standard 20 millisecond period and 1–2 millisecond duty cycle used by standard hobby servos.

Materials

For this prototype, you will need the following exact components:
* Arduino UNO R3 (ATmega328P): The primary microcontroller board.
* SG90 Micro Servo: A standard 9-gram hobby servo motor.
* 10 kΩ Potentiometer: A standard rotary potentiometer (linear taper preferred).
* Pushbutton: A standard momentary tactile switch.
* Breadboard and Jumper Wires: For making solderless connections.
* USB Type A to Type B Cable: To connect the Arduino UNO R3 to your computer.

Setup/Connection

The hardware setup is designed to be minimal, utilizing the Arduino’s internal pull-up resistor for the pushbutton to reduce external component count.

Component Wiring Guide:

Component Pin / Terminal Arduino UNO R3 Connection Notes
SG90 Servo Brown / Black Wire GND Ground connection.
SG90 Servo Red Wire 5V Power connection (Unloaded testing only).
SG90 Servo Orange / Yellow Wire Digital Pin 9 PWM control signal.
Potentiometer Left Terminal (Pin 1) 5V Reference voltage for the analog read.
Potentiometer Middle Terminal (Wiper) Analog Pin A0 Variable voltage output (0-5V).
Potentiometer Right Terminal (Pin 3) GND Ground reference.
Pushbutton Terminal 1 Digital Pin 2 Input signal.
Pushbutton Terminal 2 GND Closes circuit to ground when pressed.

Note on the Pushbutton: We connect the button directly between Digital Pin 2 and Ground. In the software, we will configure Pin 2 with INPUT_PULLUP. This holds the pin at a HIGH logic level (5V) internally. When the button is pressed, it connects the pin to ground, driving the logic level LOW.

Note on the Servo Power: The SG90 is a small servo. When completely unloaded (no mechanical linkages attached), it can safely draw power from the Arduino UNO’s 5V pin, which is supplied by the USB connection. If you attach a mechanical load to the servo, it will draw more current and may cause the Arduino to reset.

Validated Code

The project requires two files. The first is the main Arduino sketch containing the logic. The second is a simple shell script to automate the compilation and upload process using the Arduino CLI.

Main Sketch: servo_tester.ino

Create a directory named servo_tester and save the following code inside it as servo_tester.ino.

Public preview of the validated file. The complete source is shown to members and in PDF/Print.

/*
 * Project: Servo Calibration Tester
 * Target: Arduino UNO R3 (ATmega328P)
 * Description: A multi-mode servo testing tool. Uses a potentiometer for manual
 * control and a pushbutton to cycle between Manual, Center, and Sweep modes.
 */

#include <Servo.h>

// Pin Definitions
const int SERVO_PIN = 9;
const int POT_PIN = A0;
const int BUTTON_PIN = 2;

// Servo Object
Servo testServo;

// State Machine Variables
enum TesterMode {
  MODE_MANUAL = 0,
  MODE_CENTER = 1,
  MODE_SWEEP  = 2
};

TesterMode currentMode = MODE_MANUAL;

// Button Debouncing Variables
int buttonState = HIGH;             // Current reading from the input pin
int lastButtonState = HIGH;         // Previous reading from the input pin
unsigned long lastDebounceTime = 0; // The last time the output pin was toggled
const unsigned long debounceDelay = 50;   // Debounce time in milliseconds

// Servo Sweep Variables
int sweepAngle = 0;
int sweepDirection = 1;
unsigned long lastSweepUpdate = 0;
const unsigned long sweepInterval = 15; // Milliseconds between sweep steps

// Telemetry Variables
unsigned long lastTelemetryTime = 0;
const unsigned long telemetryInterval = 500; // Update Serial every 500ms

void setup() {
  // Initialize Serial Monitor
  Serial.begin(115200);
  while (!Serial) {
    ; // Wait for serial port to connect
  }

  Serial.println("Initializing Servo Calibration Tester...");

  // Configure Pins
  pinMode(POT_PIN, INPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP); // Use internal pull-up resistor

  // Attach Servo
  // Using standard min/max pulse widths (1000us to 2000us)
  // Some SG90 servos respond better to 500us to 2400us, adjust if necessary.
  testServo.attach(SERVO_PIN, 1000, 2000);

  // Set initial position
  testServo.write(90);
  Serial.println("System Ready. Mode: MANUAL");
}

void loop() {
  handleButton();
  updateServo();
  sendTelemetry();
}

void handleButton() {
  int reading = digitalRead(BUTTON_PIN);

  // Check if the button state has changed
  if (reading != lastButtonState) {
    lastDebounceTime = millis();
  }
// ... continues for members in the complete validated source ...

🔒 Part of the validated code is premium. With the 7-day pass or the monthly membership you can view the complete validated source.

/*
 * Project: Servo Calibration Tester
 * Target: Arduino UNO R3 (ATmega328P)
 * Description: A multi-mode servo testing tool. Uses a potentiometer for manual
 * control and a pushbutton to cycle between Manual, Center, and Sweep modes.
 */

#include <Servo.h>

// Pin Definitions
const int SERVO_PIN = 9;
const int POT_PIN = A0;
const int BUTTON_PIN = 2;

// Servo Object
Servo testServo;

// State Machine Variables
enum TesterMode {
  MODE_MANUAL = 0,
  MODE_CENTER = 1,
  MODE_SWEEP  = 2
};

TesterMode currentMode = MODE_MANUAL;

// Button Debouncing Variables
int buttonState = HIGH;             // Current reading from the input pin
int lastButtonState = HIGH;         // Previous reading from the input pin
unsigned long lastDebounceTime = 0; // The last time the output pin was toggled
const unsigned long debounceDelay = 50;   // Debounce time in milliseconds

// Servo Sweep Variables
int sweepAngle = 0;
int sweepDirection = 1;
unsigned long lastSweepUpdate = 0;
const unsigned long sweepInterval = 15; // Milliseconds between sweep steps

// Telemetry Variables
unsigned long lastTelemetryTime = 0;
const unsigned long telemetryInterval = 500; // Update Serial every 500ms

void setup() {
  // Initialize Serial Monitor
  Serial.begin(115200);
  while (!Serial) {
    ; // Wait for serial port to connect
  }

  Serial.println("Initializing Servo Calibration Tester...");

  // Configure Pins
  pinMode(POT_PIN, INPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP); // Use internal pull-up resistor

  // Attach Servo
  // Using standard min/max pulse widths (1000us to 2000us)
  // Some SG90 servos respond better to 500us to 2400us, adjust if necessary.
  testServo.attach(SERVO_PIN, 1000, 2000);

  // Set initial position
  testServo.write(90);
  Serial.println("System Ready. Mode: MANUAL");
}

void loop() {
  handleButton();
  updateServo();
  sendTelemetry();
}

void handleButton() {
  int reading = digitalRead(BUTTON_PIN);

  // Check if the button state has changed
  if (reading != lastButtonState) {
    lastDebounceTime = millis();
  }

  // If the state has been stable longer than the debounce delay
  if ((millis() - lastDebounceTime) > debounceDelay) {
    // If the button state has physically changed
    if (reading != buttonState) {
      buttonState = reading;

      // Only toggle mode if the new button state is LOW (pressed)
      if (buttonState == LOW) {
        cycleMode();
      }
    }
  }
  lastButtonState = reading;
}

void cycleMode() {
  if (currentMode == MODE_MANUAL) {
    currentMode = MODE_CENTER;
    Serial.println(">>> Mode Switched: CENTER (90 deg) <<<");
  } else if (currentMode == MODE_CENTER) {
    currentMode = MODE_SWEEP;
    Serial.println(">>> Mode Switched: AUTO-SWEEP <<<");
  } else {
    currentMode = MODE_MANUAL;
    Serial.println(">>> Mode Switched: MANUAL <<<");
  }
}

void updateServo() {
  switch (currentMode) {
    case MODE_MANUAL:
      {
        int potValue = analogRead(POT_PIN);
        // Map 10-bit ADC (0-1023) to Servo Angle (0-180)
        int targetAngle = map(potValue, 0, 1023, 0, 180);
        testServo.write(targetAngle);
      }
      break;

    case MODE_CENTER:
      {
        testServo.write(90);
      }
      break;

    case MODE_SWEEP:
      {
        if (millis() - lastSweepUpdate > sweepInterval) {
          lastSweepUpdate = millis();
          sweepAngle += sweepDirection;

          if (sweepAngle >= 180) {
            sweepAngle = 180;
            sweepDirection = -1;
          } else if (sweepAngle <= 0) {
            sweepAngle = 0;
            sweepDirection = 1;
          }
          testServo.write(sweepAngle);
        }
      }
      break;
  }
}

void sendTelemetry() {
  if (millis() - lastTelemetryTime > telemetryInterval) {
    lastTelemetryTime = millis();

    int currentAngle = testServo.read();
    int currentMicroseconds = testServo.readMicroseconds();

    Serial.print("Mode: ");
    switch (currentMode) {
      case MODE_MANUAL: Serial.print("MANUAL\t"); break;
      case MODE_CENTER: Serial.print("CENTER\t"); break;
      case MODE_SWEEP:  Serial.print("SWEEP \t"); break;
    }

    Serial.print(" | Angle: ");
    Serial.print(currentAngle);
    Serial.print(" deg | Pulse: ");
    Serial.print(currentMicroseconds);
    Serial.println(" us");
  }
}

Build Script: build.sh

Save this file in the parent directory of servo_tester (or adjust the path accordingly). Make it executable with chmod +x build.sh. Replace <PORT> with your actual serial port (e.g., /dev/ttyACM0 on Linux, COM3 on Windows).

#!/bin/bash

PORT="/dev/ttyACM0" # CHANGE THIS TO YOUR ACTUAL PORT
FQBN="arduino:avr:uno"
SKETCH_DIR="servo_tester"

echo "Updating Arduino core index..."
arduino-cli core update-index

echo "Installing AVR core if not present..."
arduino-cli core install arduino:avr

echo "Compiling sketch..."
arduino-cli compile --fqbn $FQBN $SKETCH_DIR

if [ $? -eq 0 ]; then
    echo "Compilation successful. Uploading to $PORT..."
    arduino-cli upload --fqbn $FQBN --port $PORT $SKETCH_DIR
    echo "Upload complete. Open your serial monitor at 115200 baud."
else
    echo "Compilation failed. Aborting upload."
    exit 1
fi

Build/Flash/Run commands

To compile and upload the firmware manually (if you choose not to use the provided shell script), follow this standardized workflow using the Arduino CLI.

Command Reference

Action Command
Update Index arduino-cli core update-index
Install Core arduino-cli core install arduino:avr
Compile arduino-cli compile --fqbn arduino:avr:uno servo_tester
Upload arduino-cli upload --fqbn arduino:avr:uno --port <PORT> servo_tester
Monitor arduino-cli monitor --port <PORT> --config baudrate=115200

Workflow

  1. Identify your port: Connect your Arduino UNO R3 to your computer. Run arduino-cli board list to find the port identifier (e.g., /dev/ttyUSB0, /dev/ttyACM0, or COM3).
  2. Prepare the environment: Run the core update and install commands to ensure your system has the latest ATmega328P toolchain.
  3. Compile the code: Execute the compile command targeting the servo_tester directory. Verify that no syntax errors are returned.
  4. Flash the device: Execute the upload command, replacing <PORT> with the port identified in step 1.
  5. Monitor the output: Launch the serial monitor command to observe the boot sequence and telemetry.

Step-by-step Validation

Once the code is uploaded and the serial monitor is open, perform the following grouped checks to validate the system.

  • Checkpoint 1: Power-on and Initialization

    • Action: Reset the Arduino.
    • Expected Observation: The serial monitor displays “Initializing Servo Calibration Tester…” followed by “System Ready. Mode: MANUAL”. The servo should snap to an initial 90-degree position.
    • Pass Condition: Telemetry begins printing every 500ms showing “Mode: MANUAL”.
  • Checkpoint 2: Manual Sweep Mode

    • Action: While in MANUAL mode, turn the potentiometer from the far left to the far right.
    • Expected Observation: The servo horn rotates smoothly in tandem with the potentiometer movement.
    • Pass Condition: The serial monitor reports angles changing smoothly from ~0° to ~180°, and pulse widths changing from ~1000 µs to ~2000 µs.
  • Checkpoint 3: Mode Switching & Centering

    • Action: Press the pushbutton once.
    • Expected Observation: The serial monitor outputs “>>> Mode Switched: CENTER (90 deg) <<<“. The servo immediately moves to its mechanical center point and locks there.
    • Pass Condition: Turning the potentiometer now has no effect on the servo. Telemetry outputs exactly 90 deg and 1500 µs.
  • Checkpoint 4: Auto-Sweep Mode

    • Action: Press the pushbutton a second time.
    • Expected Observation: The serial monitor outputs “>>> Mode Switched: AUTO-SWEEP <<<“. The servo begins oscillating back and forth automatically.
    • Pass Condition: The telemetry shows the angle incrementing to 180, reversing, decrementing to 0, and repeating.
  • Checkpoint 5: Return to Manual

    • Action: Press the pushbutton a third time.
    • Expected Observation: The system returns to MANUAL mode.
    • Pass Condition: The servo immediately snaps to the angle currently dictated by the physical position of the potentiometer wiper.

Troubleshooting

Symptom Likely Cause Fix
Servo jitters erratically Unstable power supply or poor ground connection. Ensure all ground pins (Arduino, Potentiometer, Servo) are tied to a common rail. Check jumper wires.
Arduino resets when servo moves Servo is drawing too much current (brownout). The servo is under mechanical load. Disconnect the load, or power the servo from an external 5V source (sharing grounds).
Button press is ignored / double-triggers Debounce delay is too short or wiring is loose. Increase debounceDelay in the code to 100ms. Verify the button is firmly seated in the breadboard.
Servo only turns 90 degrees total PWM pulse width mapping mismatch. Change testServo.attach(SERVO_PIN, 1000, 2000) to testServo.attach(SERVO_PIN, 500, 2400) to match your specific SG90’s limits.
Compile error: “Servo.h: No such file” Missing standard library. Run arduino-cli lib install Servo to ensure the standard library is available to the compiler.

Improvements

Once the base prototype is functioning, consider these grouped enhancements:

Hardware Upgrades
* External Power Supply: Add a dedicated 5V, 2A power supply (like a UBEC or bench supply) to the breadboard power rails to test heavy-duty, high-torque metal gear servos without browning out the Arduino.
* OLED Display: Add an I2C 0.96″ OLED screen to display the current angle and pulse width directly on the device, removing the need for a connected computer and serial monitor.

Software Enhancements
* Custom Pulse Width Tuning: Add a fourth mode that uses the potentiometer to finely adjust the minimum and maximum microsecond limits, allowing you to map the exact physical endpoints of off-brand servos.
* Speed Testing Mode: Implement a mode that commands instantaneous 0° to 180° jumps and times the physical response, helping to verify servo speed specifications.

Checklist

  • [ ] Arduino CLI is installed and the AVR core is updated.
  • [ ] Components are wired according to the Setup table (checking common ground).
  • [ ] Pushbutton is wired correctly to utilize the internal pull-up resistor.
  • [ ] servo_tester.ino is saved in the correct directory.
  • [ ] The sketch compiles successfully using arduino-cli compile.
  • [ ] The firmware uploads successfully to the correct serial port.
  • [ ] The Serial Monitor displays the initialization sequence at 115200 baud.
  • [ ] The servo responds smoothly to the potentiometer in MANUAL mode.
  • [ ] The pushbutton successfully cycles through all three operational modes.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary objective of the project described in the article?




Question 2: What microsecond value is associated with automatically centering the servo?




Question 3: Why is it important to center steering and throttle servos in RC Vehicle Setup?




Question 4: In Robotic Arm Assembly, why should you identify exact PWM microsecond values for physical joint limits?




Question 5: What hardware component is used for smooth, low-latency manual angle adjustments?




Question 6: How many operational modes does the microcontroller state machine cycle through?




Question 7: Which of the following is NOT one of the operational modes mentioned in the text?




Question 8: What is a benefit of using this tester for hardware diagnostics?




Question 9: How does the tester assist in rapid prototyping?




Question 10: What is the function of the software-debounced pushbutton in this project?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:


Practical case: RFID lock with Arduino UNO R3

Practical case: RFID lock with Arduino UNO R3 — hero

Objective and use case

What you’ll build: A basic RFID-controlled door-lock prototype using an Arduino UNO R3, MFRC522 RFID reader, and 1-channel relay. When an authorized card or key fob is detected, the Arduino validates the UID and energizes the relay for 2–5 seconds to drive an electric strike, maglock interface, or low-voltage lock circuit.

Why it matters / Use cases

  • Small workshop or cabinet access prototype: Control access to a tool cabinet, lab drawer, or demo enclosure so only known RFID tags can unlock it.
  • Entry system concept demonstrator: Practice the same flow used in larger systems: credential read, UID match, timed unlock, and relay-based lock control.
  • Electromechanical integration practice: Combine SPI-based RFID sensing with a relay output, moving beyond LED-only projects into real actuator switching.
  • Serial-monitor troubleshooting and auditing: Print detected UIDs and allow/deny events over serial at 9600 baud for fast debugging and basic access logs during testing.

Expected outcome

  • An RFID card is detected and matched in typically under 200 ms from presentation to decision.
  • The relay switches on for a configurable unlock window, commonly 3 seconds, then returns to the locked state automatically.
  • Unauthorized tags are rejected immediately, with the UID shown in the Serial Monitor for enrollment or diagnostics.
  • The prototype runs comfortably on the UNO’s 16 MHz ATmega328P with low CPU load and no GPU requirement.

Audience: Arduino beginners, students, and embedded/electronics learners building access-control demos; Level: beginner to intermediate

Architecture/flow: MFRC522 reads tag UID over SPI → Arduino UNO compares UID against an authorized list in code → if matched, digital output activates 1-channel relay → relay drives lock interface for a timed interval → Serial Monitor reports allow/deny status and UID values.

Educational validation note

Before publication, this case passed the Prometeo automated validation gate with status PASS. The validator checked the code blocks, article structure, copy/paste-safe commands and consistency with the supported device catalog.

Published validation evidence

  • Automatic result: PASS.
  • Parsed structure: 3 sections, 1 tables and 24 code blocks detected before publication.
  • Checked code: 7 C/C++ static checks, 12 Bash/copy-paste checks.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and unsupported stacks block publication.
  • Report findings: no blocking findings.

This validation confirms syntax and tool compatibility for the published material, but it does not replace physical testing on your exact hardware, wiring and runtime environment.

Educational safety note

This prototype is for educational low-voltage access-control learning. It is not a certified security product and should not be trusted as the sole protection for homes, businesses, hazardous areas, or life-safety exits.

Important limits and precautions:

  • Do not switch mains voltage directly unless you are trained and using properly rated hardware and enclosure practices.
  • For student use, validate the relay with a safe low-voltage load first.
  • RFID UID-only authorization is weak security.
  • Many low-cost RFID tags can be cloned, and UID checking alone is not secure enough for serious protection.
  • MFRC522 module voltage matters.
  • The reader is a 3.3 V device. Review your specific module’s logic tolerance before connecting it to 5 V Arduino SPI lines.
  • Real electric locks may require separate power and protection.
  • Follow the lock manufacturer’s current, voltage, and suppression requirements.
  • Do not use this project for emergency egress doors or safety-critical locks.
  • A software bug, relay failure, power loss, or wiring fault could prevent expected operation.
  • Mount and insulate wiring properly.
  • Loose wires can cause resets, false triggering, or damaged modules.

Conceptual block diagram

High-level view: what enters the system, what each block processes, and what comes out.

Functional architecture

MFRC522 reads tag UID over SPI

Arduino UNO compares UID against an autho…

if matched, digital output activates 1-ch…

relay drives lock interface for a timed i…

Serial Monitor reports allow/deny status…

Conceptual signal and responsibility flow between device blocks.

Validation path

Sketch

arduino-cli compile

Upload

Functional test

Conceptual summary of the tools used to check the published material.

Prerequisites

Before starting, you should be comfortable with:

  • Uploading a sketch to an Arduino UNO
  • Opening the Serial Monitor
  • Making simple jumper-wire connections on a breadboard or module headers
  • Understanding that a relay is an electrically controlled switch

You do not need prior RFID experience. This tutorial explains the minimum required to make a usable access-control prototype.

Materials

Use the exact device family and model requested:

  • Arduino UNO R3 (ATmega328P) + MFRC522 RFID module + 1-channel relay module

Recommended full parts list:

  • 1 x Arduino UNO R3 (ATmega328P)
  • 1 x MFRC522 RFID reader module with matching RFID card or key fob
  • 1 x 1-channel relay module
  • Prefer a module with transistor driver and opto-isolation or built-in flyback protection
  • Coil side usually powered from 5 V
  • Jumper wires
  • USB cable for Arduino UNO
  • Breadboard or stable wiring surface
  • Optional but strongly recommended for visible testing:
  • 1 x LED
  • 1 x 220 ohm resistor
  • Optional lock-side educational load:
  • Low-voltage lamp, LED indicator, or small DC load controlled through relay contacts
  • If you later connect a real electric strike or maglock interface, use the lock manufacturer’s recommended external power source

Setup/Connection

How the system works

The MFRC522 reads the UID of a nearby RFID tag over SPI.
The Arduino compares that UID against a small list of authorized UIDs stored in the sketch.
If the UID matches, the Arduino drives the relay module input for a short time.
The relay contacts can then switch a separate lock circuit or educational test load.

Important voltage note before wiring

The MFRC522 reader is a 3.3 V device. Most common breakout boards expose pins labeled SDA, SCK, MOSI, MISO, RST, 3.3V, and GND. On typical hobby tutorials, the module is powered from 3.3 V, while SPI lines connect directly to the UNO. Many students do this successfully in practice, but electrically the UNO outputs 5 V logic. If your MFRC522 board does not tolerate 5 V on logic pins, use proper level shifting. For a basic educational build, follow the common module usage pattern only if your specific module is known to work that way.

Pin mapping

Use the common MFRC522-to-UNO SPI wiring and one digital pin for the relay.

Module/Signal Arduino UNO R3 Pin Notes
MFRC522 SDA / SS D10 SPI slave select
MFRC522 SCK D13 SPI clock
MFRC522 MOSI D11 SPI MOSI
MFRC522 MISO D12 SPI MISO
MFRC522 RST D9 Reset pin for reader
MFRC522 3.3V 3.3V Do not use 5V for reader power
MFRC522 GND GND Common ground
Relay IN D7 Relay control signal
Relay VCC 5V Typical relay module power
Relay GND GND Common ground
Optional LED anode via 220 ohm resistor D6 Status indicator
Optional LED cathode GND LED return

Text-only connection steps

  1. Power off the Arduino before making or changing wiring.
  2. Connect the MFRC522:
  3. MFRC522 3.3V -> UNO 3.3V
  4. MFRC522 GND -> UNO GND
  5. MFRC522 SDA/SS -> UNO D10
  6. MFRC522 SCK -> UNO D13
  7. MFRC522 MOSI -> UNO D11
  8. MFRC522 MISO -> UNO D12
  9. MFRC522 RST -> UNO D9
  10. Connect the relay module:
  11. Relay VCC -> UNO 5V
  12. Relay GND -> UNO GND
  13. Relay IN -> UNO D7
  14. Optional visible status LED:
  15. UNO D6 -> 220 ohm resistor
  16. Resistor -> LED anode
  17. LED cathode -> GND
  18. If testing relay contacts with a safe low-voltage load:
  19. Use relay COM and NO for “normally off, on only when unlocked”
  20. Wire the low-voltage supply path through COM and NO
  21. Keep the switched circuit electrically appropriate for the relay ratings and module design

Relay contact concept

The relay module has two sides:

  • Control side
  • VCC, GND, IN
  • Connected to Arduino
  • Switching side
  • COM, NO, NC
  • Connected to the lock circuit or test load

For a door unlock action, COM + NO is usually the simplest:
– Idle: open circuit
– Authorized card: relay closes COM to NO for a few seconds

Validated Code

Arduino sketch: rfid_door_lock_relay.ino

Public preview of the validated file. The complete source is shown to members and in PDF/Print.

#include <SPI.h>
#include <MFRC522.h>

static const uint8_t SS_PIN = 10;
static const uint8_t RST_PIN = 9;
static const uint8_t RELAY_PIN = 7;
static const uint8_t STATUS_LED_PIN = 6;

// Many relay modules are ACTIVE LOW.
// Set to true if your relay turns on when the Arduino pin goes LOW.
// Set to false if your relay turns on when the Arduino pin goes HIGH.
static const bool RELAY_ACTIVE_LOW = true;

// Unlock timing in milliseconds
static const unsigned long UNLOCK_TIME_MS = 3000;

// Example authorized UIDs.
// Replace these with the UIDs printed by your own cards during enrollment/testing.
const byte AUTHORIZED_UIDS[][4] = {
  {0xDE, 0xAD, 0xBE, 0xEF},
  {0x12, 0x34, 0x56, 0x78}
};
const size_t AUTHORIZED_UID_COUNT = sizeof(AUTHORIZED_UIDS) / sizeof(AUTHORIZED_UIDS[0]);

MFRC522 mfrc522(SS_PIN, RST_PIN);

bool relayActive = false;
unsigned long relayActivatedAt = 0;
String lastUidString = "";
unsigned long lastScanAt = 0;

// Ignore repeated reads of the same card within this time window
static const unsigned long SAME_CARD_DEBOUNCE_MS = 1500;

void setRelay(bool on) {
  relayActive = on;

  if (RELAY_ACTIVE_LOW) {
    digitalWrite(RELAY_PIN, on ? LOW : HIGH);
  } else {
    digitalWrite(RELAY_PIN, on ? HIGH : LOW);
  }

  digitalWrite(STATUS_LED_PIN, on ? HIGH : LOW);
}
// ... continues for members in the complete validated source ...

🔒 Part of the validated code is premium. With the 7-day pass or the monthly membership you can view the complete validated source.

#include <SPI.h>
#include <MFRC522.h>

static const uint8_t SS_PIN = 10;
static const uint8_t RST_PIN = 9;
static const uint8_t RELAY_PIN = 7;
static const uint8_t STATUS_LED_PIN = 6;

// Many relay modules are ACTIVE LOW.
// Set to true if your relay turns on when the Arduino pin goes LOW.
// Set to false if your relay turns on when the Arduino pin goes HIGH.
static const bool RELAY_ACTIVE_LOW = true;

// Unlock timing in milliseconds
static const unsigned long UNLOCK_TIME_MS = 3000;

// Example authorized UIDs.
// Replace these with the UIDs printed by your own cards during enrollment/testing.
const byte AUTHORIZED_UIDS[][4] = {
  {0xDE, 0xAD, 0xBE, 0xEF},
  {0x12, 0x34, 0x56, 0x78}
};
const size_t AUTHORIZED_UID_COUNT = sizeof(AUTHORIZED_UIDS) / sizeof(AUTHORIZED_UIDS[0]);

MFRC522 mfrc522(SS_PIN, RST_PIN);

bool relayActive = false;
unsigned long relayActivatedAt = 0;
String lastUidString = "";
unsigned long lastScanAt = 0;

// Ignore repeated reads of the same card within this time window
static const unsigned long SAME_CARD_DEBOUNCE_MS = 1500;

void setRelay(bool on) {
  relayActive = on;

  if (RELAY_ACTIVE_LOW) {
    digitalWrite(RELAY_PIN, on ? LOW : HIGH);
  } else {
    digitalWrite(RELAY_PIN, on ? HIGH : LOW);
  }

  digitalWrite(STATUS_LED_PIN, on ? HIGH : LOW);
}

void printUid(const MFRC522::Uid *uid) {
  for (byte i = 0; i < uid->size; i++) {
    if (uid->uidByte[i] < 0x10) {
      Serial.print("0");
    }
    Serial.print(uid->uidByte[i], HEX);
    if (i < uid->size - 1) {
      Serial.print(":");
    }
  }
}

String uidToString(const MFRC522::Uid *uid) {
  String s = "";
  for (byte i = 0; i < uid->size; i++) {
    if (uid->uidByte[i] < 0x10) {
      s += "0";
    }
    s += String(uid->uidByte[i], HEX);
    if (i < uid->size - 1) {
      s += ":";
    }
  }
  s.toUpperCase();
  return s;
}

bool isAuthorized(const MFRC522::Uid *uid) {
  // This basic example checks only 4-byte UIDs against a fixed list.
  if (uid->size != 4) {
    return false;
  }

  for (size_t i = 0; i < AUTHORIZED_UID_COUNT; i++) {
    bool match = true;
    for (byte j = 0; j < 4; j++) {
      if (uid->uidByte[j] != AUTHORIZED_UIDS[i][j]) {
        match = false;
        break;
      }
// ... continues for members in the complete validated source ...

Library dependency installation

The MFRC522 library is not part of the standard built-in Arduino core, so install it explicitly with Arduino CLI.

arduino-cli lib install "MFRC522"

Build/Flash/Run commands

The following commands match the required Arduino CLI workflow for Arduino UNO R3.

1) Update board index

arduino-cli core update-index

2) Install the AVR core

arduino-cli core install arduino:avr

3) Install the RFID library

arduino-cli lib install "MFRC522"

4) Create the sketch folder

Example directory:

mkdir -p rfid-door-lock-relay

Save the sketch as:

rfid-door-lock-relay/rfid_door_lock_relay.ino

5) Compile

Run this from the parent directory of the sketch folder, or specify the full path.

arduino-cli compile --fqbn arduino:avr:uno rfid-door-lock-relay

6) Find your serial port

On Linux:

arduino-cli board list

On Windows, the port may appear like COM3, COM4, etc.
On Linux, something like /dev/ttyACM0 or /dev/ttyUSB0.
On macOS, something like /dev/cu.usbmodem....

7) Upload

Replace <PORT> with your actual detected port.

arduino-cli upload --fqbn arduino:avr:uno --port <PORT> rfid-door-lock-relay

8) Open serial monitor

arduino-cli monitor --port <PORT> --config baudrate=9600

Step-by-step Validation

This section helps you prove the prototype behaves as intended.

1. Validate the compile stage

Goal:
– Confirm the sketch syntax, board target, and installed library are correct.

Procedure:
1. Run:
bash
arduino-cli compile --fqbn arduino:avr:uno rfid-door-lock-relay

2. Expected result:
– Compilation completes successfully.
– No missing-library errors for MFRC522.h.

What this validates:
– The code is structurally valid for Arduino UNO.
– The library dependency is correctly installed.

What this does not validate:
– Wiring
– Card readability
– Relay polarity
– Physical lock compatibility

2. Validate idle startup behavior

Goal:
– Confirm the board boots and the relay starts in the locked/idle state.

Procedure:
1. Upload the sketch.
2. Open the serial monitor at 9600 baud.
3. Reset the board if needed.

Expected log pattern:

RFID Door Lock Relay Prototype
Present a card/tag to the MFRC522 reader.
Known 4-byte UIDs will activate the relay.

Expected hardware behavior:
– Relay should be idle, not continuously energized.
– Optional LED should be off.

If the relay is ON at idle:
– Your module may use opposite input logic.
– Change:
cpp
static const bool RELAY_ACTIVE_LOW = true;

to:
cpp
static const bool RELAY_ACTIVE_LOW = false;

– Recompile and upload again.

3. Discover your card UID

Goal:
– Read the actual UID of your RFID card or key fob.

Procedure:
1. Present a card to the reader.
2. Watch the serial monitor.

Expected output example:

Card detected. UID=93:4A:1C:7F
ACCESS DENIED - unauthorized card

Record the UID exactly.

Notes:
– The provided code example authorizes only 4-byte UIDs.
– Many common MIFARE cards work this way, but some tags may have different UID lengths.
– For a basic project, use a card with a 4-byte UID if possible.

4. Add your real authorized UID

Goal:
– Make your card unlock the relay.

Procedure:
1. Edit this section:
cpp
const byte AUTHORIZED_UIDS[][4] = {
{0xDE, 0xAD, 0xBE, 0xEF},
{0x12, 0x34, 0x56, 0x78}
};

2. Replace one line with your actual UID bytes.
Example, if the monitor showed 93:4A:1C:7F:
cpp
const byte AUTHORIZED_UIDS[][4] = {
{0x93, 0x4A, 0x1C, 0x7F}
};

3. Recompile and upload.

5. Validate successful access

Goal:
– Confirm that an authorized card activates the relay for the configured duration.

Procedure:
1. Present the authorized card once.
2. Watch the serial output.
3. Listen for relay click and observe optional LED.

Expected output:

Card detected. UID=93:4A:1C:7F
ACCESS GRANTED - relay ON for 3000 ms
Relay OFF - lock returned to idle state

Expected measurable behavior:
– Relay turns on once
– Relay remains active for about 3 seconds
– Relay returns to idle automatically

6. Validate denied access

Goal:
– Ensure an unlisted card does not unlock.

Procedure:
1. Present a different RFID card or fob.
2. Watch the serial monitor.

Expected output:

Card detected. UID=11:22:33:44
ACCESS DENIED - unauthorized card

Expected hardware behavior:
– No unlock relay action
– Optional LED remains off

7. Validate repeat-read handling

Goal:
– Confirm the same card held near the reader does not rapidly retrigger.

Procedure:
1. Hold an authorized card on the reader continuously.
2. Observe behavior for a few seconds.

Expected result:
– One unlock event should occur.
– The debounce window avoids repeated immediate re-triggering of the same read.

Troubleshooting

RFID reader is not detecting any card

Check:

  • MFRC522 powered from 3.3 V, not 5 V
  • SPI pins are exactly:
  • D10 -> SDA/SS
  • D11 -> MOSI
  • D12 -> MISO
  • D13 -> SCK
  • D9 -> RST
  • Card is compatible with MFRC522 reader frequency and type
  • Grounds are common between all modules

Symptom:
– No UID messages at all in Serial Monitor

Likely causes:
– Wrong SPI wiring
– Missing library not likely if code already uploaded
– Reader power issue
– Poor jumper contact

Relay stays on all the time

Likely cause:
– Relay module polarity is opposite your code setting

Fix:
– Change:
cpp
static const bool RELAY_ACTIVE_LOW = true;

to:
cpp
static const bool RELAY_ACTIVE_LOW = false;

– Recompile and upload

Authorized card still says denied

Check:

  • UID bytes copied correctly
  • Hex values include 0x
  • Card has 4-byte UID
  • You re-uploaded after editing

Serial output shows UID in lowercase or mixed style

That is fine as long as byte values are correct. The matching logic uses raw bytes, not the printed format.

Relay clicks but your lock or test load does not activate

Check the switching side:

  • Are you using COM and NO?
  • Is the external low-voltage supply present?
  • Is the load circuit complete through relay contacts?
  • Is the load within relay current/voltage rating?

Arduino resets when relay switches

Possible reasons:

  • USB power is weak
  • Relay module causes noise or supply dip
  • Wiring is too loose or too long

Try:

  • Shorter wires
  • Better 5 V supply for the relay module if appropriate
  • Ensure common ground is solid
  • Test with only relay module first, then add lock-side load

Improvements

Once the basic prototype works, these are useful next steps:

Add separate status LEDs

Use:
– Green LED for granted
– Red LED for denied

This improves usability without needing the Serial Monitor.

Add a buzzer

A short beep for valid access and a different beep pattern for denied access makes the system feel more realistic.

Add enrollment mode

Instead of hardcoding UIDs, you could:
– press a button,
– scan a master card,
– store new UIDs in EEPROM.

That would make the prototype more practical.

Add a door sensor

A magnetic reed switch can detect whether the door actually opened or remained open too long.

Improve security logic

This project checks only card UID. For a more realistic system, you could explore:
– sector authentication concepts,
– master/admin cards,
– anti-passback ideas,
– event logging to external storage.

Add a lockout timer

After several denied attempts, the system could ignore scans for 30 seconds and blink a warning LED.

Final Checklist

Use this checklist before calling the project finished:

  • [ ] Arduino UNO R3 is wired correctly
  • [ ] MFRC522 is powered from 3.3 V
  • [ ] Relay module is wired to D7, 5V, and GND
  • [ ] Common ground exists between Arduino, RFID module, and relay module
  • [ ] Sketch saved as rfid_door_lock_relay.ino
  • [ ] MFRC522 library installed with Arduino CLI
  • [ ] Project compiles with:
    bash
    arduino-cli compile --fqbn arduino:avr:uno rfid-door-lock-relay
  • [ ] Project uploads with:
    bash
    arduino-cli upload --fqbn arduino:avr:uno --port <PORT> rfid-door-lock-relay
  • [ ] Serial Monitor at 9600 baud shows startup text
  • [ ] Unknown card produces ACCESS DENIED
  • [ ] Authorized card produces ACCESS GRANTED
  • [ ] Relay activates for about 3000 ms
  • [ ] Relay returns to idle automatically
  • [ ] Low-voltage test load switches correctly through relay contacts
  • [ ] You understand this is an educational prototype, not a certified security system

With that, you have a practical RFID door-lock relay prototype that a beginner can actually assemble, test, and extend into a more realistic access-control project.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:

Quick Quiz

Question 1: What is the content property value for the pseudo-element in '.prometeo-educational-note h3::before'?




Question 2: What is the border-left-color of the '.prometeo-safety-note' class?




Question 3: What is the margin-bottom value for '.prometeo-educational-note p:last-child'?




Question 4: What is the border-radius value specified for '.prometeo-educational-note'?




Question 5: What is the background color of the '.prometeo-educational-note code' element?