Motor Speed Tachometer

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.

CIRCUIT DIAGRAM:

Motor Speed Tachometer
  • L298N Motor Driver:
    • 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)
  • Light Blocking Sensor:
    • 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.

    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.