Measuring rotational speed accurately is a foundational skill in motor control, automotive diagnostics, and wind turbine monitoring. An Arduino RPM gauge built with a Hall effect sensor and a hardware interrupt offers a massive advantage over optical alternatives: it is completely immune to ambient light, dust, and oil mist. By counting magnetic pulses via the microcontroller's interrupt vector, you can achieve sub-millisecond timing resolution without blocking your main loop.

This guide walks through building a high-resolution RPM gauge targeting the Arduino Nano v3 (ATmega328P), complete with an I2C OLED readout, noise-rejection wiring practices, and a robust debugging framework for when your readings inevitably jitter or flatline.

Project Overview & Bill of Materials

Difficulty Rating: Intermediate (Requires soldering, interrupt logic, and I2C troubleshooting)
Estimated Build Time: 45 minutes
Target Board: Arduino Nano v3 (ATmega328P, 16MHz)

To ensure reliable signal edges and prevent I2C bus lockups, do not skip the decoupling capacitor or the pull-up resistor. The A3144 is an open-drain sensor; without a pull-up, the signal line will float, causing phantom interrupts.

Component Exact Variant / Spec Est. Cost (2026) Purpose
Microcontroller Arduino Nano v3 (ATmega328P) $6.00 Main logic, hardware interrupts
Hall Sensor A3144EUA-T (Unipolar, Open-Drain) $1.20 Magnetic pulse detection
Display SSD1306 0.96" I2C OLED (128x64) $5.50 Visual RPM readout
Magnet N42 Neodymium (6mm x 2mm disc) $1.50 Triggers the Hall sensor
Passives 10kΩ Resistor, 0.1µF Ceramic Cap $0.10 Signal pull-up and noise decoupling

Pin Mapping & Wiring Procedure

Before applying power, verify your I2C wiring. The SSD1306 OLED operates at 3.3V logic internally but most breakout boards include a regulator and logic level shifters, allowing you to power it from the Nano's 5V rail safely.

Module Module Pin Arduino Nano Pin Notes
SSD1306 OLEDGNDGNDCommon ground is critical for I2C
SSD1306 OLEDVCC5VVerify breakout has onboard regulator
SSD1306 OLEDSCLA5Hardware I2C clock line
SSD1306 OLEDSDAA4Hardware I2C data line
A3144 SensorVCC (Pin 1)5VAdd 0.1µF cap between Pin 1 and 3
A3144 SensorOUT (Pin 3)D2INT0 pin. Requires 10kΩ pull-up to 5V

Numbered Wiring Steps

  1. Prep the Sensor: Solder the 0.1µF ceramic capacitor directly across the VCC and GND pins of the A3144. This shunts high-frequency EMI from the motor brushes.
  2. Install Pull-Up: Connect the 10kΩ resistor between the A3144 OUT pin (Pin 3) and the 5V rail. The A3144 can only pull the line low; the resistor pulls it high.
  3. Wire the Interrupt: Connect the A3144 OUT pin to D2 on the Nano. D2 maps to hardware interrupt INT0. Do not use analog pins or D4-D13 for high-speed pulse counting.
  4. Magnet Placement: Glue the N42 magnet to your rotating shaft. The A3144 is unipolar, meaning it only triggers on the South pole. Test polarity with a compass or spare sensor before applying CA glue or epoxy.

Complete Arduino RPM Gauge Code

The following code is written specifically for the Arduino Nano v3 (ATmega328P). It uses micros() instead of millis() to maintain resolution at high RPMs (above 10,000 RPM, a 1ms resolution introduces severe quantization error). We also include a cross-platform macro for the interrupt attribute, ensuring the code won't throw memory errors if you later port it to an ESP32.

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// Cross-platform interrupt attribute macro
#if defined(ESP32)
  #define INTERRUPT_ATTR IRAM_ATTR
#else
  #define INTERRUPT_ATTR
#endif

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C // Check your specific module (sometimes 0x3D)

#define HALL_SENSOR_PIN 2     // Must be an interrupt-capable pin (D2 or D3 on Nano)
#define PULSES_PER_REV 1      // Change if using multiple magnets

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

volatile unsigned long lastMicros = 0;
volatile float currentRPM = 0.0;
float smoothedRPM = 0.0;
const float alpha = 0.3; // EMA filter weight (0.0 to 1.0)

INTERRUPT_ATTR void pulseISR() {
  unsigned long now = micros();
  if (lastMicros > 0) {
    unsigned long delta = now - lastMicros;
    // 60,000,000 microseconds in a minute
    currentRPM = (60000000.0 / delta) / PULSES_PER_REV;
  }
  lastMicros = now;
}

void setup() {
  Serial.begin(115200);
  
  // Initialize I2C Display with error handling
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    // Halt execution if display fails to initialize
    for(;;); 
  }
  
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(2);
  display.setCursor(0, 20);
  display.println("RPM GAUGE");
  display.display();
  delay(1000);

  // Configure Hall Sensor Pin
  pinMode(HALL_SENSOR_PIN, INPUT_PULLUP); // Internal pull-up as backup
  
  // Attach hardware interrupt on FALLING edge
  attachInterrupt(digitalPinToInterrupt(HALL_SENSOR_PIN), pulseISR, FALLING);
}

void loop() {
  // Apply Exponential Moving Average (EMA) to smooth low-RPM jitter
  noInterrupts();
  float rawRPM = currentRPM;
  interrupts();

  if (rawRPM > 0) {
    smoothedRPM = (alpha * rawRPM) + ((1.0 - alpha) * smoothedRPM);
  } else {
    // Timeout logic: if no pulse for 1 second, RPM is 0
    noInterrupts();
    unsigned long timeSinceLast = micros() - lastMicros;
    interrupts();
    if (timeSinceLast > 1000000) {
      smoothedRPM = 0;
    }
  }

  // Update OLED
  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println("Motor Speed");
  
  display.setTextSize(3);
  display.setCursor(0, 25);
  display.print((int)smoothedRPM);
  
  display.setTextSize(1);
  display.setCursor(90, 40);
  display.println("RPM");
  display.display();

  // Serial output for plotting
  Serial.print("Raw:"); Serial.print(rawRPM);
  Serial.print("\tSmooth:"); Serial.println(smoothedRPM);

  delay(100); // Update screen at 10Hz
}

Debugging: First Three Checks & Common Errors

When your RPM gauge reads zero, maxes out, or throws compilation errors, follow this diagnostic sequence. Do not rewrite your code until you have verified the physical layer.

The First Three Things to Check

  1. Magnet Polarity: The A3144 is unipolar. If you glued the North pole facing the sensor, it will never trigger. Flip the magnet or swap to an omnipolar sensor like the DRV5055.
  2. Interrupt Pin Mapping: Verify you are using D2 or D3. Using D4 with attachInterrupt() on an ATmega328P will silently fail to trigger, leaving your RPM at zero.
  3. I2C Address Mismatch: Use the Adafruit I2C Scanner sketch to confirm if your OLED is at 0x3C or 0x3D. Cheap clones frequently ship with the alternate address.

Exact Error Strings & Ranked Causes

Error String: SSD1306 allocation failed
Ranked Causes:
1. The I2C address in SCREEN_ADDRESS does not match the hardware (0x3C vs 0x3D).
2. The OLED VCC is unpowered, or the SDA/SCL lines are swapped.
3. The Adafruit_SSD1306 library is outdated and conflicting with a newer Adafruit_GFX version.
Error String: ISR not in IRAM! (Followed by a Guru Meditation Error / Core Panic)
Context: This occurs if you port the exact code above to an ESP32 without the cross-platform macro.
Ranked Causes:
1. The interrupt handler function lacks the IRAM_ATTR directive, meaning the ESP32 tried to execute the ISR from slower flash memory instead of RAM.
2. You are calling Serial.println() or delay() inside the ISR. Never use blocking functions inside an interrupt vector.

For a deeper understanding of how hardware interrupts map to AVR vectors, consult the official Arduino attachInterrupt() documentation.

Extending and Simplifying the Build

How to Simplify

If you are integrating this into a larger system or just need to log data, drop the OLED entirely. Remove the Adafruit libraries, delete the display.* calls, and rely solely on the Serial.print() statements. Open the Arduino IDE's Serial Plotter (Ctrl+Shift+L) to visualize the RPM curve in real-time. This frees up roughly 4KB of flash memory and eliminates I2C bus contention.

How to Extend

At very low speeds (under 50 RPM), the time delta between pulses becomes large, and slight mechanical variations in magnet placement cause the calculated RPM to jump wildly. The code above includes an Exponential Moving Average (EMA) filter to solve this. To tune it, adjust the alpha variable. A lower alpha (e.g., 0.1) provides heavier smoothing but introduces lag; a higher alpha (e.g., 0.8) makes the gauge more responsive but jittery.

To extend the hardware, add a second Hall sensor offset by 90 degrees. By checking which sensor triggers first, you can determine the direction of rotation, turning this from a simple tachometer into a full quadrature encoder system.

Frequently Asked Questions

Can I use an optical sensor instead of a Hall effect sensor for my Arduino RPM gauge?

Yes, you can use a reflective optical sensor like the TCRT5000 or a slotted optocoupler (e.g., LM393 speed sensor module). However, optical sensors are highly susceptible to ambient sunlight and require precise alignment with a piece of reflective tape on the shaft. For enclosed, dusty, or outdoor environments (like a bicycle wheel or lawnmower engine), the magnetic Hall effect approach is vastly superior and requires zero line-of-sight alignment.

Why is my Arduino RPM gauge reading double the actual speed?

This almost always happens for one of two reasons. First, you may have accidentally attached two magnets to the shaft (or the shaft itself has multiple magnetic poles). Second, if you are using a digital Hall sensor with a push-pull output (rather than open-drain) and you configured the interrupt to trigger on CHANGE instead of FALLING, the microcontroller is counting both the rising and falling edges of the square wave. Change the interrupt mode to FALLING and ensure PULSES_PER_REV matches your physical magnet count.

How do I calibrate an Arduino RPM gauge for a multi-pole motor?

Brushless DC (BLDC) motors and stepper motors have multiple magnetic pole pairs. If you are reading the magnetic field directly from the motor's rotor without a separate 1:1 timing gear, you must divide the raw pulse count by the number of pole pairs. For example, a standard hobby outrunner motor often has 14 poles (7 pole pairs). In the code, you would change #define PULSES_PER_REV 1 to #define PULSES_PER_REV 7. Always verify against a known-good optical tachometer when calibrating multi-pole setups.