Motor Speed Tachometer
HARDWARE REQUIRED:
- PICUNO Microcontroller board
- 1 × DC Motor with encoder disk (20 shafts)
- 1 × 298N Motor Driver
- 1 × 10kΩ Potentiometer
- 1 × Light Blocking sensor
- 1 × 4xAA Battery pack (with fresh or rechargeable batteries)
- Jumper wires
- USB cable
DESCRIPTION:
This project creates a functional tachometer, a device that measures the rotational speed of a motor in Revolutions Per Minute (RPM).
A small disk with one or more slots is attached to the motor shaft. This disk spins through the Light Blocking Sensor's slot. Each time a slot in the disk passes through the sensor, the infrared beam is unblocked, generating a digital pulse. The PICUNO uses hardware interrupts for precise pulse counting. It counts the number of pulses over a set period, calculates the RPM, and prints the result to the Serial Monitor in real-time. A potentiometer is used as a throttle to control the motor's speed, allowing you to instantly see how the RPM changes.
A small disk with one or more slots is attached to the motor shaft. This disk spins through the Light Blocking Sensor's slot. Each time a slot in the disk passes through the sensor, the infrared beam is unblocked, generating a digital pulse. The PICUNO uses hardware interrupts for precise pulse counting. It counts the number of pulses over a set period, calculates the RPM, and prints the result to the Serial Monitor in real-time. A potentiometer is used as a throttle to control the motor's speed, allowing you to instantly see how the RPM changes.
CIRCUIT DIAGRAM:
- OUT1 & OUT2: Connect these two screw terminals to the outputs for the DC Motor. Connect the two wires from one DC motor here.
- Connect the positive terminal (+) of the 4xAA battery pack to the 12V screw terminal.
- Connect the negative terminal (-) of the 4xAA battery pack to the GND screw terminal.
- Also connect the GND terminal on the L298N to a GND pin on the microcontroller to create a common ground.
- Connect the IN1 pin (Left motor) to GPIO 8.
- Connect the IN2 pin (Left motor) to GPIO 9.
- Connect the ENA pin (Left motor speed) to GPIO 10.
- Connect the IN1 pin (Right motor) to GPIO 11.
- Connect the IN2 pin (Right motor) to GPIO 12.
- Connect the ENB pin (Right motor speed) to GPIO 13.
NOTE: Remove the ENA and ENB jumpers on the L298N motor driver.
- Connect outer terminals of the potentiometer to VCC and GND, centre terminal to Analog pin A0 (Pin 26 in PICUNO)
- Connect the GND (-) pin to GND on board.
- Connect the VCC (+) pin to 5V on board.
- Connect the Signal (S) pin to GPIO 6.
SCHEMATIC:
L298N Motor Driver:
OUT2 & OUT2 → Outputs for the DC Motor.
12V → positive terminal (+) of 4xAA battery pack.
GND → negative terminal (-) of 4xAA battery pack → GND on PICUNO.
IN1 (Left Motor) → GPIO 8
IN2 (Left Motor) → GPIO 9
ENA (Left Motor speed) → GPIO 10
IN3 (Right Motor) → GPIO 11
IN4 (Right Motor) → GPIO 12
ENB (Right Motor) → GPIO 13
10kΩ Potentiometer:
Outer Terminals → VCC, GND
Centre Terminal → A0 (GPIO 26)
Light Blocking sensor:
GND / (-) → GND
VCC / (+) → 5V
Signal / (S) → GPIO 6
CODE -- C:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 const int SENSOR_PIN = 6; const int POT_PIN = A0; const int MOTOR_ENA = 10; const int MOTOR_IN1 = 8; const int MOTOR_IN2 = 9; // --- Tachometer Settings --- const int SLOTS_IN_DISK = 2; volatile unsigned long pulseCount = 0; unsigned long lastTime = 0; int rpm = 0; // --- Interrupt Service Routine (ISR) --- void countPulse() { pulseCount++; } void setup() { Serial.begin(9600); Serial.println("RPM Tachometer Ready"); pinMode(MOTOR_IN1, OUTPUT); pinMode(MOTOR_IN2, OUTPUT); pinMode(MOTOR_ENA, OUTPUT); // Set motor to spin forward digitalWrite(MOTOR_IN1, HIGH); digitalWrite(MOTOR_IN2, LOW); pinMode(SENSOR_PIN, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(SENSOR_PIN), countPulse, FALLING); } void loop() { // 1. Control motor speed with the potentiometer int potValue = analogRead(POT_PIN); int motorSpeed = map(potValue, 0, 1023, 0, 255); analogWrite(MOTOR_ENA, motorSpeed); // 2. Calculate and display RPM every second if (millis() - lastTime >= 1000) { detachInterrupt(digitalPinToInterrupt(SENSOR_PIN)); // Calculate RPM (pulses per second * 60 seconds) / slots per revolution rpm = (pulseCount * 60) / SLOTS_IN_DISK; // Display the result on the Serial Monitor Serial.print("RPM: "); Serial.println(rpm); // Reset for the next measurement pulseCount = 0; lastTime = millis(); // Re-enable the interrupt attachInterrupt(digitalPinToInterrupt(SENSOR_PIN), countPulse, FALLING); } }
countPulse() function - This is the Interrupt Service Routine (ISR). It's a special, high-priority function that the microcontroller pauses its main loop() to run immediately whenever the sensor detects a pulse. Its only job is to increment the pulseCount.
attachInterrupt(...) - It tells the PICUNO to constantly monitor the SENSOR_PIN and to automatically run the countPulse() function every time it detects a FALLING edge (the signal going from high to low).
if (millis() - lastTime >= 1000) - The millis() function returns the number of milliseconds since the board started. This line checks if one second has passed without using a delay() command, which would freeze the program and cause pulses to be missed.
detachInterrupt() / attachInterrupt() - Before reading and resetting pulseCount, the interrupt is temporarily detached. This ensures the ISR can't change the value while the main loop is in the middle of a calculation. After the calculation, the interrupt is re-attached to resume counting.
attachInterrupt(...) - It tells the PICUNO to constantly monitor the SENSOR_PIN and to automatically run the countPulse() function every time it detects a FALLING edge (the signal going from high to low).
if (millis() - lastTime >= 1000) - The millis() function returns the number of milliseconds since the board started. This line checks if one second has passed without using a delay() command, which would freeze the program and cause pulses to be missed.
detachInterrupt() / attachInterrupt() - Before reading and resetting pulseCount, the interrupt is temporarily detached. This ensures the ISR can't change the value while the main loop is in the middle of a calculation. After the calculation, the interrupt is re-attached to resume counting.
CODE -- PYTHON:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 from machine import Pin, ADC, PWM import time sensor_pin = Pin(6, Pin.IN, Pin.PULL_UP) pot = ADC(Pin(26)) motor_ena = PWM(Pin(10)) motor_in1 = Pin(8, Pin.OUT) motor_in2 = Pin(9, Pin.OUT) motor_ena.freq(1000) # --- Tachometer Settings --- SLOTS_IN_DISK = 20 pulse_count = 0 last_time = time.ticks_ms() rpm = 0 def count_pulse(pin): global pulse_count pulse_count += 1 sensor_pin.irq(trigger=Pin.IRQ_FALLING, handler=count_pulse) # --- Main Program --- print("RPM Tachometer Ready") motor_in1.high() motor_in2.low() while True: # 1. Control motor speed with the potentiometer pot_value = pot.read_u16() motor_ena.duty_u16(pot_value) # 2. Calculate and display RPM every second if time.ticks_diff(time.ticks_ms(), last_time) >= 1000: irq_state = machine.disable_irq() temp_pulse_count = pulse_count pulse_count = 0 # Re-enable interrupt machine.enable_irq(irq_state) rpm = (temp_pulse_count * 60) // SLOTS_IN_DISK print(f"RPM: {rpm}") last_time = time.ticks_ms() time.sleep_ms(20)
count_pulse(pin) function - This is the Interrupt Service Routine (ISR). It's a special, high-priority function that runs automatically the instant a pulse is detected by the sensor pin. The global pulse_count line is essential, allowing this function to modify the main pulse_count variable.
sensor_pin.irq(...) - This line sets up the hardware interrupt. It tells the PicUNO to monitor the sensor_pin and, when it detects a FALLING edge (the signal going from high to low as a slot passes), to immediately run the count_pulse function.
if time.ticks_diff(...) >= 1000: - This is a non-blocking timer. It checks if at least one second has passed since the last calculation without using time.sleep(), which would freeze the program.
machine.disable_irq() / machine.enable_irq() - This block temporarily "pauses" all interrupts just long enough to safely copy the pulse_count value and then reset it to zero.
sensor_pin.irq(...) - This line sets up the hardware interrupt. It tells the PicUNO to monitor the sensor_pin and, when it detects a FALLING edge (the signal going from high to low as a slot passes), to immediately run the count_pulse function.
if time.ticks_diff(...) >= 1000: - This is a non-blocking timer. It checks if at least one second has passed since the last calculation without using time.sleep(), which would freeze the program.
machine.disable_irq() / machine.enable_irq() - This block temporarily "pauses" all interrupts just long enough to safely copy the pulse_count value and then reset it to zero.