Controlling a motorcycle tachometer with a raspberry pi

7th December 2015

I run this blog on a 2012 Raspberry Pi home-server running FreeBSD. I wanted a physical display of how many visitors it's had in the last day. Now in order not to deviate from the "technology is bad" theme I didn't want to use some fancy LED display, rather something more low-tech.

1. The gauge

I got the cheapest motorcycle tachometer I could find off eBay. It's an unbranded Chinese one. Obviously the quality is fantastic.

Now the actual mechanism inside uses an electromagnet to deflect the needle. But of course it's a tachometer so it has circuitry to read engine speed using a wire going to the ignition system and converts that into a current for the analog gauge.

Not wanting to make circuitry to emulate an engine ignition system, I decided to get the analog gauge out so I could control it directly with a DAC (digital-to-analog converter).

Now unfortunately the case was made of steel pieces bent together which made it almost indestructible. The only way I could get it open was to break the glass face and pry the rim away bit-by-bit with pliers.

see the workings in the right-hand image. the face is at the top. below are two yellow wires to control it. below that two brass coil springs, which also carry the current to the electromagnet. the electromagnet itself is in the plastic thing. the fixed magnet is hidden in the bottom

OK so once I removed the gauge and detached it from all it's circuitry I found that it is controlled by a current between about 0-43mA. Since the coil is resistive you can also control it with a voltage; between 0-5.6V.

2. The interface

Now to control it. Normally one might do this using PWM, but I couldn't make PWM work using FreeBSD on a 2012 Pi. I could have done software PWM but I didn't want to waste the Pi's limited processing resources. Also remember it takes 5.6V to max out the gauge and the GPIO can provide a maximum of 5 so I would need extra circuitry and an external power-supply anyway.

A sensible person would probably just buy an 8-bit DAC (for example a TLC7524) and just use 8 of the Pi's GPIO pins to send it digital values. But it's awesomer to design and make my own circuitry to do the job:

the transistor terminals are labelled c=collector, b=base, e=emitter

The coil in the drawing represents the gauge. The BD135 BJT transistor is the magic of the circuit, it controls the current through the gauge. Now the base-emitter voltage of a BJT is fairly constant at about 0.7V and the voltage of the GPIO pins is 3.3V when on, so the amount of current that flows is 2.6V (3.3-0.7) divided by the value of the resistor on the GPIO pin. Therefore each pin contributes an amount of current dependant on the value of it's resistor. BJTs are current amplifiers so this current is multiplied by the transistor's hFE (DC current gain), powering the gauge.

Notice that the value of each resistor is double the last, because in binary each extra bit is worth double. So LSB (least-significant bit) is the 5M632 resistor and MSB is the 22K. I made these strange value resistors by connecting two or three in series. If you make this yourself remember that the precision of these resistance values is important. I got all mine to within 0.5% by using an Ohmmeter and hand-picking the resistors.

The diode is to protect the transistor against any reverse voltage if the gauge is switched off suddenly (electromagnets make reverse voltage when switched off suddenly). I used a zener type diode so it can also protect the gauge against over-voltage if something goes wrong. The capacitor across the power supply is not strictly necessary, it just smooths the power from the switch-mode power supply I'm using (just a random old laptop power-supply I had lying around).

made on prototype strip-board thing. note Pi GPIO breakout board on right. note nice aluminium heat-sink on BD135. note mess of resistors all crammed in.

3. The software

Well FreeBSD provides a great little program called gpioctl for controlling GPIO. Use gpio -c $pin OUT to switch pins to output mode. Use gpio $pin 1 to switch a pin on.

Now see the script I wrote to control the gauge. It's quite well commented although the comments amusingly contain profanity which for fun I leave in.

#! /usr/local/bin/bash


# This script controls an analog gauge from a motorcycle
# tachometer. Uses a really shit home-made DAC with 9-bit precision
# giving 512 values.

# The pins must have already been set to output mode by gpioctl

# Is it a number between 0 and 13000?
if [[ "${1}" == ?(-)+([0-9]) ]] && (( "${1}" < 13001 )) && (( "${1}" > -1 ))
    echo "First arg must be a number between 0 and 13000"
    exit 1

# This stage fixes... err problems with accuracy due to the low quality
# of my DAC and the effects of gravity on the gauge.
if (( input > 1000 )) && (( input <= 7000 ))
    # From 1000 to 7000 we ramp up to an addition of 500
    ramp_multiplier=$(( input - 1001 ))
    add=$(( ( ramp_multiplier * 500 ) / 6000 ))
    (( debug_accuracyfix )) && echo adding $add
    input=$(( input + add ))
elif (( input > 7000 )) && (( input <= 10500 ))
    # From 7000 to 10500 we ramp down from an additon of 500 to 0
    ramp_multiplier=$(( 10500 - input ))
    add=$(( ( ramp_multiplier * 500 ) / 3500 ))
    (( debug_accuracyfix )) && echo adding $add
    input=$(( input + add ))
elif (( input > 10500 )) && (( input <= 11750 ))
    # From 10500 to 11750 we ramp up to a subtraction of 400
    ramp_multiplier=$(( input - 10501 ))
    rem=$(( ( ramp_multiplier * 400 ) / 1250 ))
    (( debug_accuracyfix )) && echo removing $rem
    input=$(( input - rem ))
elif (( input > 11750 )) && (( input <= 13000 ))
    # From 11750 to 13000 we ramp down from a subtraction of 400 to 0
    ramp_multiplier=$(( 13000 - input ))
    rem=$(( ( ramp_multiplier * 400 ) / 1250 ))
    (( debug_accuracyfix )) && echo removing $rem
    input=$(( input - rem ))

# Now we need to make a 9 bit value (0-511) to control the gauge

# A signal less than 100 has no effect because of the transistor's
# "emitter cut-off current". A signal greater than 490 maxes out the gauge

# Adapt our 0-13000 to between 100 and 490
input=$(bc -l <<< "(( ${input} / 13000 ) * 390 ) + 100")

# Now round it to an integer
input=$(echo "${input}" | awk '{printf("%d\n",$1 + 0.5)}')

# Convert it to binary
out=$(printf "%09d\n" $(bc <<< "obase=2 ; ${input}"))

#                  msb                   lsb
declare -a pinarr=(15 14 18 23 24 25 8 7 11)

declare -a outarr

while read -n 1 var
done <<< "${out}"

# Update each pin
for i in {0..8}
    (( debug_rawoutput )) && echo "gpioctl ${pinarr[${i}]} ${outarr[${i}]}"
    gpioctl ${pinarr[${i}]} ${outarr[${i}]}

The final piece of the puzzle is a script that runs every morning, getting statistics on total visitors from GoAccess (a log analyzer) and using the above script to update the gauge. Each 1000RPM represents 10 visitors. I haven't put the script here since it's trivial but email me if you want more info.

37 visitors yesterday

Well, here it is, working. Maybe I'll make it a proper case one-day.

Further entertainment

If you wonder where I got the "x10visits" sticker, I made it by sticking a postit-note onto a piece of paper and printing it with a normal printer. You have to make sure it gets printed on the sticky part. If you do this I highly recommend getting the expensive postit-notes as they stick better.

I tried using this to display CPU usage just for fun, but my gauge_control script uses too much CPU itself, I think you'd have to write a C program to do it.

2 weeks later...

This turned out to be the most popular post I've ever written, getting on the front page of Hacker News and generating 6000 views in a few hours! This of course maxed out the gauge by two orders of magnitude.

I have now updated the gauge with a logarithmic display so it won't max out, and made it a hardboard case so I don't break it. Unfortunately the case looks ugly and overized but ah well.