Raspberry Pi Tachometer Uses Python & Hall Effect Sensor

I’ve been building large inexpensive outdoor robotic rovers that can carry a heavy payload for miles.  I needed a way to measure the speed of the wheels and use that information to calculate how fast the bot is moving.  I wanted software that was written with Python 3 and which would run on a Raspberry Pi.  And I was looking for a simple and easy to understand program because I’m a very new Python user and I haven’t done any programming for a very long time.

So I searched the net and rejected most of the Python scripts I found because they were too complex for me to understand and modify.  I’d also learned that I wanted a script that used interrupts.  I finally found one that met my requirements, but it has a major flaw.  It can’t tell when the wheel has stopped moving, so it keeps printing out the last RPM value that was measured, even when the RPM is zero.  My version fixes that problem.

I’m using a Hall effect sensor that detects a magnet that is attached to one of the gears in the gear train.  That gear turns five times every time the wheel turns once, so my script includes a variable called “adjustment” that is used to calculate the wheel’s RPM.  I use different size wheels, so there’s another variable for the wheel diameter so the bot’s speed in miles per hour can be calculated correctly.

The script also counts the number of interrupts, which occur whenever the sensor detects the magnet.  And because 5 interrupts are generated for every wheel rotation, the sensor can be used as a very low-resolution encoder.  If you know how much each of the wheels have moved, then you can use that information to navigate a robotic rover fairly accurately for short distances when GPS is unavailable or not accurate enough. 

This script stores some of the data in a file so it can be analyzed later, because I may not be able to use WiFi or other means to view it in real-time.  Eventually, I will be collecting voltage, current, temperature, and IMU data.  That data will be then be used to select the motors and/or gear ratios needed to maximize either range or speed, or achieve a good compromise.  I still have a lot to learn about DC motors and I’m looking for someone who’d be willing to mentor me.

I’m using inexpensive KY-003 Hall Effect Magnetic Sensor Modules, but you should be able to use this script with any Hall Effect or optical sensor that can be read by a GPIO pin.  I’ll provide more details about the sensor, and my bots, after I’ve done more testing and development.  In the meantime, you can easily find information elsewhere about how to connect and use them.

The output of my sensor and script match the readings from a commercially made digital tachometer that uses an optical sensor.  I have not tried using it to measure very high RPMs yet. 

# Last Update: 02/23/2020 at 1813 hours

import RPi.GPIO as GPIO
from time import sleep, strftime, time    # strtime & time only for file output
import time, math
import os    # needed to find out if output directory already exists
from datetime import datetime   # needed to add time and date to output file

gpio_pin_number = 21   # GPIO Pin used by sensor
wheel_diameter_in = 10   # wheel diameter in inches
adjustment = 1   # adjustment for gear ratio or number of magnets 
seconds = 1   # time to wait between printing values 
rpm = 0
mph = 0
elapsed_time = 0   # amount of time a full revolution takes
number_interrupts = 0   # counts interrupts (triggered by sensor)
previous_number_interrupts = 0
start_timer = time.time()

parent_dir = "/home/pi/Desktop/"  # parent directory path 
directory = "RPM_logs/"    # new directory name - slash at end is important
file_name = "RPM_log_"    # file name prefix for log file

path = os.path.join(parent_dir, directory)   # full path to log file 

# print (f"path= {path}")   # use to check directory path

try:  # create directory if it doesn't exist, tell us if it already does
    print("Directory '% s' created" % directory)
except FileExistsError:
    print("folder already exists")

filename = file_name + str(datetime.now().strftime('%Y_%m_%d_%H%M%S')) + '.csv'
filepath = str(path + filename)
# print(filepath)   # uncomment to check file path

def init_GPIO(): # initialize GPIO

def calculate_elapsed_time(channel): 
    global number_interrupts, start_timer, elapsed_time, signal
    number_interrupts+=1    # increase by 1 whenever an interrupt occurs
    elapsed_time = time.time() - start_timer   # time each rotation takes
    start_timer = time.time()   # Set start_time to current time

def calculate_speed(wheel_diameter):
    global number_interrupts, elapsed_time, rpm, mph
    if elapsed_time !=0:   # avoid DivisionByZero error
        rpm = (1/elapsed_time * 60) * adjustment
        wheel_circumf_in = math.pi * wheel_diameter_in   # wheel circumference in inches
        mph = (rpm * wheel_circumf_in) / 1056
def init_interrupt():
    GPIO.add_event_detect(gpio_pin_number, GPIO.FALLING, callback = calculate_elapsed_time, bouncetime = 20)

if __name__ == '__main__':
with open(filepath, "a") as log:

    while True:

        if number_interrupts != previous_number_interrupts:
           calculate_speed(wheel_diameter_in)   # call this function with wheel diameter as parameter
           print (f"RPM = {round(rpm)}, MPH = {round(mph,1)}")
           log.write("{0},{1},{2}\n".format(str(round(rpm)),(str(round(mph,1))),strftime("%Y-%m-%d %H:%M:%S")))   # write RPM and time to log file

        if (number_interrupts == previous_number_interrupts): # no new interrupts, so rpm & mph = 0
            rpm = 0
            mph = 0.0
            print (f"RPM = {round(rpm)}, MPH = {round(mph,1)}")
            log.write("{0},{1}\n".format(str(round(rpm)),strftime("%Y-%m-%d %H:%M:%S")))  # write RPM and time to log file
        previous_number_interrupts = number_interrupts 

        sleep(seconds)   # wait before displaying & writing more data

How the program works – An explanation for Dummies and Python Newbies

(I’m sorry, but I’m not going to explain the code that is used to create the log file and write data to it.)

This program is different from many other programs that measure RPM because it uses interrupts. 

It starts by importing some libraries that it needs, which are already installed if you’re using Raspbian. 

It then initializes some variables.  There are four that you may need to edit:

sensor = 21   #GPIO Pin the sensor is connected to
wheel_diameter_in = 10   #inches
seconds = .2   # time to wait between printing the RPM and speed
Adjustment = 1 

The adjustment variable should set to one if you’re using just one magnet that is attached a wheel or its axle.  If you put the magnet on a gear or pulley then you’ll have to figure out what number you need to multiple the gear’s RPM by to get the RPM of the wheel.  In my case, the gear turns 5 times faster than the wheel, so I have to multiply the gear rpm by .2 to get the wheel RPM (1/5).

The script then creates the functions that it will use.

 if __name__ == ‘__main__’:  is where the fun starts.  If you haven’t seen this before, then just think of it as the “main loop” where the script spends most of its time.

First, init_GPIO() calls a function that initializes the GPIO pin and tells it how to act.  It uses the sensor variable which tells it what GPIO pin your sensor is attached to. This only happens once.

Then, init_interrupt() calls a function that tells the Raspberry Pi to start looking for something to happen on the GPIO pin and then do something. In this case, if it sees the voltage on the pin start to drop from its usual 5-volt “high” it goes to the calculate_elapse function, which is a “call back” function.  As a Python newbie I’ve had some trouble understanding what a “call back” function is.  All you need to know, if you’re not familiar with them, is that it’s basically a function that’s is called by another function.

The nice thing about using interrupts is that the Raspberry Pi will keep looking for the voltage to drop on the GPIO pin, without being told to, and go to the call back function every time it sees one If it didn’t do that then you’d have to set up some kind of loop that would constantly keep reading the pin.  While your program is busy doing that it wouldn’t be able to do much else, and you might miss signals from the sensor if you didn’t read the pin often enough. 

So the Raspberry Pi keeps looking for interrupts and every time it sees one the calculate_elapse function figures out how much time has passed since the last interrupt.  The time is then stored in a  global variable called elapsed_time so other parts of the script can easily use the information.  Some other information is also stored in global variables.

Then we get to the While True loop.  It keeps asking the calculate_speed function to use the elapsed_time variable to calculate the RPM and the speed in miles per hour, then it prints out that information and stores it in a file. 

But…. it only calls the calculate_speed function if we keep getting new interrupts, which will only happen when the gear is turning and the Hall sensor is detecting the magnet.  The number of interrupts are counted and stored in a global variable called number_interrupts. If the gear stops rotating then the number of interrupts will stop increasing.

To see if it’s increasing or not, we store the current value of number_interrupts in a variable near the end of the While True loop called previous_number_interrupts.  The next time through the loop, number_interrupts and previous_number_interrupts are compared.  If they’re different, then the gear must still be turning, so we need to call the calculate_speed function and have it calculate the RPM and MPH.  If they’re not different, then the gear/wheel has stopped turning and we don’t need to calculate anything, because we know the rpm and mph is zero.

The script then prints out and stores whatever the rpm and mph is.

We don’t need the script to print the rpm and mph as fast as it can.  So at the end of the loop we wait a little while, and you can adjust the amount of time by changing the sleep variable.  

Leave a Comment