Community project

Hall-Effect RPM Monitor

ESP32
Photo of Hall-Effect RPM Monitor
Generated with AI

Juan David Artunduaga Diaz

Published October 14, 2026

This project builds a real-time RPM monitor for a brushed DC motor using a Hall-effect sensor to detect shaft rotation. The ESP32 reads the sensor pulses, calculates RPM with noise filtering, and displays the result on a 16×2 LCD while a potentiometer lets you adjust motor speed via PWM control.

The guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions covering sensor placement, op-amp signal conditioning, motor driver setup, and firmware configuration. By the end, you'll have a working tachometer that measures motor speed accurately and handles electrical noise rejection.

Wiring diagram

Wiring diagram for Hall-Effect RPM Monitor

Gather all the parts

QtyComponent
1

SS49E Linear Hall Effect Sensor

Ratiometric linear Hall-effect sensor IC in a TO-92 package. Outputs an analog voltage proportional to the applied magnetic flux density, centered around half the supply voltage at zero field.

1

LCD 16x2 I2C

16x2 character LCD display with I2C backpack

1

IRLZ44N Logic-Level N-Channel MOSFET

Logic-level N-channel MOSFET commonly used as a low-side switch for DC loads from 3.3V or 5V microcontroller GPIO. Use a gate resistor, a gate pulldown, and a flyback diode for inductive loads.

1

USB-C 5V Adapter

USB-C wall adapter delivering regulated 5 V to the board's USB or VBUS rail. Default wired power source for desktop / stationary projects.

1

MCP6002 dual rail-to-rail op-amp

A dual rail-to-rail amplifier IC; channel A turns the Hall voltage around its zero-field level into a 0 to 3.3 V signal.

1

MCP6561 rail-to-rail push-pull comparator

A single-supply comparator that turns the conditioned Hall waveform into a clean 3.3 V logic edge with deliberate switching margin.

1

5 V brushed DC motor

A small brushed DC motor whose shaft carries one small diametrically magnetized magnet for one pulse per revolution.

1

10 kΩ linear potentiometer

A three-terminal adjustable resistor whose knob sets the requested motor speed.

1

1N5819 Schottky flyback diode

A diode across the motor that safely carries the motor current for a moment when switching stops.

1

24 kΩ 1% resistor

24 kΩ

A fixed 1% resistor forming the Hall differential amplifier input ratio.

1

56 kΩ 1% resistor

56 kΩ

A fixed 1% resistor forming the Hall differential amplifier feedback ratio.

1

10 kΩ 1% resistor

10 kΩ

The upper fixed resistor of the 2.5 V Hall zero-field reference divider.

1

10 kΩ 1% resistor

10 kΩ

The lower fixed resistor of the 2.5 V Hall zero-field reference divider.

1

10 kΩ 1% resistor

10 kΩ

The upper fixed resistor of the 1.65 V logic-midpoint reference divider.

1

10 kΩ 1% resistor

10 kΩ

The lower fixed resistor of the 1.65 V logic-midpoint reference divider.

1

100 Ω gate resistor

100 Ω

A resistor that limits the brief charging current into the MOSFET gate.

1

100 kΩ gate pulldown resistor

100 kΩ

A resistor that keeps the motor switched off while the ESP32 is starting.

1

100 kΩ Schmitt input resistor

100 kΩ

A fixed resistor that feeds the conditioned Hall signal into the comparator threshold node.

1

1 MΩ Schmitt feedback resistor

1 MΩ

A fixed resistor that feeds back comparator output to create a 0.33 V noise margin.

1

24 kΩ 1% resistor

24 kΩ

A fixed 1% resistor carrying the 2.5 V zero-field reference into the Hall differential amplifier.

1

56 kΩ 1% resistor

56 kΩ

A fixed 1% resistor that injects the 1.65 V output midpoint into the Hall differential amplifier.

1

Bidirectional I²C level-shifter module

A four-channel bidirectional level-shifter board that keeps the LCD’s 5 V I²C pull-ups away from the ESP32’s 3.3 V pins.

Assemble it in 7 steps

1. Place the shaft magnet and Hall sensor

Fix one small diametrically magnetized magnet to the motor shaft. Hold the flat face of hall_ss49e 2–5 mm from the magnet’s path, with the sensor’s marked face aimed at the magnet. Wire VCC → 5 V (power), GND → GND (ground), and OUT → the 24 kΩ resistor r_gain_in (signal).

  • Turn the shaft slowly by hand first; reversing the sensor face reverses the waveform but does not damage it.
  • Keep the magnet firmly attached: a loose magnet can be thrown from the spinning shaft.

2. Build the Hall amplifier

Power opamp_mcp6002 with VDD → 5 V (power) and VSS → GND (ground). Make the 2.5 V reference from r_vq_top and r_vq_bottom: 5 V → r_vq_top → shared midpoint → r_vq_bottom → GND. Connect r_gain_in from Hall OUT to A_IN+, r_gain_ref from the 1.65 V midpoint to A_IN+, r_gain_in_vq from the 2.5 V midpoint to A_IN−, and r_gain_fb from A_OUT back to A_IN−.

  • Use four separate 1% resistors for the 24 kΩ/56 kΩ matched ratios; do not replace them with series or parallel combinations.
  • Make sure the 5 V and GND pins are not swapped — swapped power can damage the amplifier.

3. Make the clean 3.3 V pulse

Power comparator_mcp6561 from 3V3 and GND. Join r_schmitt_in from the amplifier output to IN+, and r_schmitt_fb from OUT back to IN+. Connect IN− to the 1.65 V midpoint made by r_vref_top and r_vref_bottom. Connect comparator OUT → ESP32 GPIO27 (speed signal).

  • The comparator output is already a safe 0–3.3 V signal for the ESP32; never connect the 5 V Hall output directly to GPIO27.
  • Do not omit the common ground: without it the pulse has no shared voltage reference and the speed reading will jump.

4. Wire the motor switch

Connect motor_5v M+ → 5 V (power) and M− → motor_mosfet DRAIN (switched return). Connect motor_mosfet SOURCE → GND (ground). Put flyback_1n5819 across the motor: cathode → 5 V and anode → M−. Connect ESP32 GPIO25 through r_gate to the MOSFET gate, and r_gate_pull from the same gate to GND.

  • The diode’s striped end is its cathode; it goes to the motor’s 5 V side.
  • A reversed flyback diode shorts the supply when the motor turns on and can damage the adapter, diode, or wiring.

5. Wire the speed knob

Connect pot_10k END_A → 3V3 (power), END_B → GND (ground), and the middle WIPER → ESP32 GPIO34 (signal). Turning the knob changes the motor PWM command.

  • If clockwise rotation lowers the speed, swap the two outside potentiometer connections.
  • Use 3.3 V, not 5 V, on the potentiometer: GPIO34 must never receive more than 3.3 V.

6. Connect the LCD safely

Power lcd from 5 V and GND. Put i2c_level_shifter between it and the ESP32: shifter LV → 3V3 (power), HV → 5 V (power), GND → GND (ground), LV_SDA → GPIO21 (data), LV_SCL → GPIO22 (data), and connect LCD SDA/SCL to the matching HV_SDA/HV_SCL pins.

  • The level-shifter is necessary because the LCD’s pull-up resistors use 5 V while the ESP32 pins only accept 3.3 V.
  • Do not connect the LCD’s 5 V SDA or SCL wires directly to the ESP32 — 5 V can damage its inputs.

7. Apply power and check the motion

Connect supply_5v to the 5 V rail and GND rail. Plug the ESP32 into USB so it has its normal board power, while keeping all grounds joined. The screen should show motor duty and measured RPM; turn the knob slowly and watch the reading change.

  • Start at a low knob setting and keep fingers, loose wires, and clothing away from the rotating shaft.
  • Use a supply that can provide the motor’s stall current; an undersized supply can reset the ESP32 or overheat.

Review all connections

1. Connections between "supply_5v" and "ESP32"

Functionsupply_5vESP32
power+5V5V
groundGNDGND

2. Connections between "hall_ss49e" and "ESP32"

Functionhall_ss49eESP32
powerVCC5V
groundGNDGND
analogOUT → 24 kΩ 1% resistor AEXT

3. Connections between "opamp_mcp6002" and "ESP32"

Functionopamp_mcp6002ESP32
powerVDD5V
groundVSSGND
analogA_IN+ → 24 kΩ 1% resistor BEXT
analogA_IN- → 24 kΩ 1% resistor BEXT
analogA_OUT → 56 kΩ 1% resistor AEXT

4. Connections between "r_gain_in_vq" and "ESP32"

Functionr_gain_in_vqESP32
analogA → 10 kΩ 1% resistor BEXT

5. Connections between "r_vq_top" and "ESP32"

Functionr_vq_topESP32
powerA5V

6. Connections between "r_vq_bottom" and "ESP32"

Functionr_vq_bottomESP32
analogA → 10 kΩ 1% resistor BEXT
groundBGND

7. Connections between "r_gain_fb" and "ESP32"

Functionr_gain_fbESP32
analogB → MCP6002 dual rail-to-rail op-amp A_IN-EXT

8. Connections between "r_gain_ref" and "ESP32"

Functionr_gain_refESP32
analogA → 10 kΩ 1% resistor BEXT
analogB → MCP6002 dual rail-to-rail op-amp A_IN+EXT

9. Connections between "r_vref_top" and "ESP32"

Functionr_vref_topESP32
powerA3V3

10. Connections between "r_vref_bottom" and "ESP32"

Functionr_vref_bottomESP32
analogA → 10 kΩ 1% resistor BEXT
groundBGND

11. Connections between "comparator_mcp6561" and "ESP32"

Functioncomparator_mcp6561ESP32
powerVDD3V3
groundVSSGND
analogIN- → 10 kΩ 1% resistor BEXT
digitalOUTGPIO 27

12. Connections between "r_schmitt_in" and "ESP32"

Functionr_schmitt_inESP32
analogA → MCP6002 dual rail-to-rail op-amp A_OUTEXT
analogB → MCP6561 rail-to-rail push-pull comparator IN+EXT

13. Connections between "r_schmitt_fb" and "ESP32"

Functionr_schmitt_fbESP32
digitalA → MCP6561 rail-to-rail push-pull comparator OUTEXT
analogB → MCP6561 rail-to-rail push-pull comparator IN+EXT

14. Connections between "pot_10k" and "ESP32"

Functionpot_10kESP32
powerEND_A3V3
groundEND_BGND
analogWIPERGPIO 34

15. Connections between "r_gate" and "ESP32"

Functionr_gateESP32
digitalAGPIO 25
digitalB → IRLZ44N Logic-Level N-Channel MOSFET GATEEXT

16. Connections between "r_gate_pull" and "ESP32"

Functionr_gate_pullESP32
digitalA → IRLZ44N Logic-Level N-Channel MOSFET GATEEXT
groundBGND

17. Connections between "motor_mosfet" and "ESP32"

Functionmotor_mosfetESP32
dataDRAIN → 5 V brushed DC motor M-EXT
groundSOURCEGND

18. Connections between "motor_5v" and "ESP32"

Functionmotor_5vESP32
powerM+5V

19. Connections between "flyback_1n5819" and "ESP32"

Functionflyback_1n5819ESP32
dataANODE → 5 V brushed DC motor M-EXT
powerCATHODE5V

20. Connections between "i2c_level_shifter" and "ESP32"

Functioni2c_level_shifterESP32
powerLV3V3
powerHV5V
groundGNDGND
i2cLV_SDAGPIO 21
i2cLV_SCLGPIO 22

21. Connections between "lcd" and "ESP32"

FunctionlcdESP32
powerVCC5V
groundGNDGND
i2cSDA → Bidirectional I²C level-shifter module HV_SDAEXT
i2cSCL → Bidirectional I²C level-shifter module HV_SCLEXT

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

constexpr uint8_t HALL_EDGE_PIN = 27;
constexpr uint8_t POT_PIN = 34;
constexpr uint8_t MOTOR_PWM_PIN = 25;
constexpr uint8_t I2C_SDA_PIN = 21;
constexpr uint8_t I2C_SCL_PIN = 22;
constexpr uint8_t PULSES_PER_REV = 1;
constexpr uint32_t PWM_FREQUENCY_HZ = 20000;
constexpr uint8_t PWM_BITS = 10;
constexpr uint16_t PWM_MAX = (1U << PWM_BITS) - 1U;
constexpr uint32_t STOP_TIMEOUT_US = 2500000UL;
constexpr uint8_t PERIOD_AVERAGES = 8;

LiquidCrystal_I2C lcd(0x27, 16, 2);

volatile uint32_t lastEdgeUs = 0;
volatile uint32_t capturedPeriodUs = 0;
volatile bool periodReady = false;

uint32_t periodSamples[PERIOD_AVERAGES] = {0};
uint8_t sampleCount = 0;
uint8_t sampleIndex = 0;
uint32_t lastScreenUpdateMs = 0;
float shownRpm = -1.0f;
int shownDuty = -1;

void IRAM_ATTR hallEdgeISR() {
  const uint32_t nowUs = micros();
  if (lastEdgeUs != 0) {
    const uint32_t candidateUs = nowUs - lastEdgeUs;
    // Reject implausibly short pulses caused by electrical noise.
    if (candidateUs >= 1000UL) {
      capturedPeriodUs = candidateUs;
      periodReady = true;
    }
  }
  lastEdgeUs = nowUs;
}

float averagedRpm() {
  if (sampleCount == 0) return 0.0f;
  uint64_t sumUs = 0;
  for (uint8_t i = 0; i < sampleCount; ++i) sumUs += periodSamples[i];
  const float averagePeriodUs = static_cast<float>(sumUs) / sampleCount;
  if (averagePeriodUs <= 0.0f) return 0.0f;
  return 60000000.0f / (averagePeriodUs * PULSES_PER_REV);
}

float roundToHalfRpm(float value) {
  return roundf(value * 2.0f) / 2.0f;
}

void drawScreen(float rpm, int dutyPercent) {
  lcd.setCursor(0, 0);
  lcd.print("RPM: ");
  lcd.print(rpm, 1);
  lcd.print("       ");
  lcd.setCursor(0, 1);
  lcd.print("Motor: ");
  lcd.print(dutyPercent);
  lcd.print("%       ");
}

void setup() {
  pinMode(HALL_EDGE_PIN, INPUT);
  pinMode(POT_PIN, INPUT);
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  lcd.init();
  lcd.backlight();
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Hall RPM meter");
  lcd.setCursor(0, 1);
  lcd.print("Starting...");

  ledcAttach(MOTOR_PWM_PIN, PWM_FREQUENCY_HZ, PWM_BITS);
  ledcWrite(MOTOR_PWM_PIN, 0);
  attachInterrupt(digitalPinToInterrupt(HALL_EDGE_PIN), hallEdgeISR, RISING);
}

void loop() {
  const int potRaw = analogRead(POT_PIN);
  const uint16_t pwmDuty = map(potRaw, 0, 4095, 0, PWM_MAX);
  const int dutyPercent = static_cast<int>((100UL * pwmDuty + PWM_MAX / 2) / PWM_MAX);
  ledcWrite(MOTOR_PWM_PIN, pwmDuty);

  if (periodReady) {
    noInterrupts();
    const uint32_t periodUs = capturedPeriodUs;
    periodReady = false;
    interrupts();
    periodSamples[sampleIndex] = periodUs;
    sampleIndex = (sampleIndex + 1) % PERIOD_AVERAGES;
    if (sampleCount < PERIOD_AVERAGES) ++sampleCount;
  }

  float rpm = 0.0f;
  const uint32_t edgeAgeUs = micros() - lastEdgeUs;
  if (lastEdgeUs != 0 && edgeAgeUs <= STOP_TIMEOUT_US) rpm = roundToHalfRpm(averagedRpm());

  const uint32_t nowMs = millis();
  if (nowMs - lastScreenUpdateMs >= 200 && (rpm != shownRpm || dutyPercent != shownDuty)) {
    drawScreen(rpm, dutyPercent);
    shownRpm = rpm;
    shownDuty = dutyPercent;
    lastScreenUpdateMs = nowMs;
  }
}

Remix this project

Make it yours in one click

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

Open in Schematik