Speedo Cable Driver

Hello folks,

I am looking for some guidance on building a speedometer cable driver. I would like to make it open source so that anyone who has an older car they are modifying can make it work for their application. I am going to be putting a different transmission in the car so I will lose the speedo cable driver, but this is also beneficial for folks who just want to change their tire size.

My initial thoughts are to use a small controller like a nano. I need to drive a single stepper motor, and pickup signal from a wheel speed sensor.

The car I am doing this to is a 1995 Mercedes E300D. It does have wheel speed sensors for the front wheels. They are magnetic pickups with a tone ring. The car is pre OBD2 so it has no communication. I was planning on tapping one of the wheel speed sensors to get my ground speed input.

I am going to build in gearing for the stepper. Usually a speedometer cable spins about one thousand times per mile, but being a german car it may be one thousand rotations per kilometer. I'll burn that bridge when I get there. Steppers lose a fair amount of torque as they spin faster, and usually only want to spin to about 1000 RPM. At 60 miles per hour, that would be 1000RPM on the stepper, so I was going to make a 2:1 or 3:1 gearing to allow for a complete range of the speedometer.

Since it's an automotive application I want to use a good stepper driver to keep the noise down. I already have some TMC2100s (IIRC) on hand. I have a 3D printer and the design skills to make the enclosure.

I have done minimal coding, but with a little guidance I can probably write the code needed to get the thing I want. I do have a couple of questions.

There is probably a header I can include for stepper control so that I don't have to write that code. Anyone know what it is? Sorry if my terminology is wrong, it's been quite some time since I did any coding.

The wheel speed sensor inevitably kicks out a series of high and low pulses. I'm not sure whether they are 5V or 12V, I'll hook up my osciliscope to it and spin the wheel to see what it reads. What kind of signal processing do I need to do so it doesn't kill the arduino when I wire it up? Would this be an analogue or digital input?

Any pointers or ideas would be greatly appreciated. I am essentially a novice when it comes to the electronics side of this stuff. Thanks for reading!

Why do you need to use a stepper motor?
That seems like a questionable choice for this application.

Welcome to the forum. That is quite a project, but it should be doable within the Arduino system.

First thing though, before you do any coding, is to write down in plain language what you want to do with the program:

  • Read a sensor
  • Convert it to some value
  • Display it
    Then break it down into smaller parts, write code, test each part.

As per @lastchancename, the doesn't make sense to me.
Or are you using it to display the result, e.g move a speedo needle to a specific position. In that case, it does not need to have gearing, etc. Most steppers are 200 steps per revolution and many stepper drivers can micro-step to get the resolution you want.

Many people like the moba-tools library. It allows stepper control. Though you could use accelStepper or just the stepper library.

If you are receiving discrete pulses, then reading them as digital pulses is exactly what you want. But, being an automotive system, it will likely be 12V if it is pre-wired for power. Definitely check with your scope. If it is 12V, then cannot connect directly to the input pins. May need a voltage divider or some other form of level shifting. An opto-coupler could do that and provide some noise control too.

1 Like

You might check into this, I am not sure it will have any effect. " Altering the mileage reading on a motor vehicle is a felony . Effective July 5, 1994, the odometer tampering statutes were recodified from Title 15, U.S.C., to Title 49."

@lastchancename I wanted to use a stepper for torque and a precise speed control. Running a 3:1 will reduce the availabe torque, but it should still be sufficient. I considered using a brushless dc motor and a optical sensor to make sure it's giving the desired speed. I may still do this or use an optical sensor to get a closed loop on output.

@cncnotes in plain english here's what I want to do.

1 - Read a wheel speed sensor
2 - Convert wheel speed sensor from Hz to ground speed
3 - Output ground speed to stepper motor to turn the speedo cable

Let's just say the wheel speed sensor has 20 highs and 20 lows per revolution. I would use the tire size to figure out how far the tire has travelled. let's say the tire is 26.5" in diameter. So about 48k pulses per mile, or 800Hz at 60Mph.

For stepper output at that ground speed, assuming 1000 revolutions per mile, with a 3:1 gear that would should be about 333rpm.

There's no screens necessary, just an input frequency and an output speed sent through a cable. Here's a link to an already existing unit folks can buy, but I like building stuff so I'm not going to buy it.

Gilshultz, I am not altering the mileage I am keeping an accurate mileage. What that refers to is winding an odometer backwards or replacing an instrument cluster with one that reads a lower mileage. If the functionality is correct there's no problem

Don’t dismiss just getting the existing speedo recalibrated .
Another option is to dismantle and drive the needle with a stepper .

Driving the needle directly with a stepper wont work because then odometer won't work. I won't be able to recalibrate the existing speedo because I am replacing the transmission with something that doesn't support a speedo cable.

Although, perhaps I could drive the needle with a stepper and the odometer separately, but that seems like more work than just making one stepper drive the whole unit.

Thanks for explaining that; I just learned something!

So, in answer to your plan:

  1. Should be easy to do with an Uno or Nano, as long as voltages are kept in mind.
    A simple voltage divider would work. But, the thing for you to determine is the nominal voltage of the signal, as well as the maximum voltage that could come in to that line.
    Likely it can go higher, such as 14V or so. In that case, you could put a 4.7V or 5.1V Zener diode in parallel with the lower resistor.
    Alternatively, an optocoupler such as PC817 with the appropriate current limiting resistor for the 12V on the input side.

  2. The output from 1 is read in with the Arduino, using code such as this one here. Obviously, you can ignore all the display code. A freq input of 800 Hz should not be a problem at all.

  1. Using one of the stepper driver libraries. And a suitable stepper driver. I'm sure you should be able to tap the car's 12V for power for the driver and the stepper.

Could you please clarify if you want the speedo shaft to be spinning at 1000 rpm? You are certainly right about the higher rpm and steppers. A simple belt and pulley arrangement should be good. You may even want a 4:1 gearing ratio, since steppers lose torque at higher speeds.
And don't forget to multiply the torque needed from the stepper by the same ratio.

1 Like

No problem, sometimes I laugh at how simple and practical old school things are and how overlcomplicated new stuff is.

Definitely something I will come back to once I get the code hammered out and the stepper motor spinning based on some input. I'll probably just use a 555 timer to make some arbitrary pulses to get everything working without having to implement it into the vehicle.

Most automotive systems are nominally 12V, but are usually about 14.7V with a healthy alternator. Having data/ sensor lines run 5V is pretty common too. I might get lucky with having a german car and it is a well manicured signal, but I also might end up with problems tapping that signal. We'll see what happens, either I will have to down regulate it, figure out how to read the signal without pulling much current, or it will just be fine and easy. I should probably do something about regulating it to be a decent signal though.

Thanks for this, I was reading through the reference documentation and came across the pulseIn() Advance I/O. I figured this would be the perfect thing for reading a pin for a signal. I tend to be a bit of a butcher when it comes to code. I just find something that does part of what I want and modify it. That's partially because I don't understand enough code to know what I am doing, and partially because it feels pragmatic to me. Like for the first sketch of this, I just grabbed the code from the stepper library for stepper speed control and played around with it a bit. It's probably not right, but it gets me started haha.

Almost certainly, IIRC it's pretty common to drive steppers with higher voltages. It might not even care if I run it at 14.7 volts. I might have to play with current values or something of the sort but either way, I'll start worrying about that structure once I have the code doing the thing I want.

So, usually the speedo shaft is calibrated for 1000rpm to be 60mph. The way I initially wrote it was not conscise, the speedometer and odometer are driven off the same cable. In the US, it is common for 1000 revolutions of the shaft to equal one mile. If we are driving 60 miles per hour, that's a mile a minute, which equates to 1000 rotations per minute. This is just me guessing how the Germans would have done it, but I would guess that 100kmh is 1000rpm. Which is roughly the same speed, it's like 62 miles per hour IIRC.

I considered a 4:1 ratio as well, There will probably be some good choice between torque and final speed for a given speedo. I will likely only ever drive about 80mph, but the gauge goes to 140 so I might as well be able to get to 140mph. Since a stepper is overly percise for this application, even higher might be better It will probably come down to what selection of gates belts and pulleys are there that fit reasonably in an enclosure.

So here's my loose game plan:
-Tinker with the code until I think it does what I expect it to
-Build it on a bread board
-Figure out what the automotive signals look like and emulate those
-Trouble shoot it until it behaves as expected
-Make sure the stepper can drive the speedo
-Make an expansion board and enclosure
-Install it and probably trouble shoot it some more.

1 Like

So, here's some code I've been scratching my head on. What I am trying to do is create a float called gaugeSpeed that is a map which correlates a pulse duration to a ground speed. I am getting kind of confused about this though. Usually a map would be from zero to some value, but in this case the pulse would be zero when stopped, longer when going slow and shorter when going fast.

Maybe I am going about this wrong, and I am certainly a novice when it comes to code some I won't be surprised if I am putting things in the wrong place.

Anyway, my thought was if I set the map to be from some maximum value (slow wheel speed) to some minimum value (fast wheel speed) then I could correlate some pulse length to a desired output speed from zero to guageMax. From what I've found pulseIn() only outputs a duration, although I feel like a frequency output would be better for this application. Then my map would be from 0Hz to max frequency. Maybe I missed something, but the reference doesn't mention anything about frequency.

I would then use a bit of math on gaugeSpeed to get the desired output speed from the stepper.

I'll end up changing the name of gaugeSpeed to wheelSpeed that way gaugeSpeed is the actual output for the stepper.

Pin selection is not important to me right now, they're just place holders at the moment. This is code from stepper speed control from the stepper library that I just started butchering as I stated before.

Another aside, am I wrong to want to use a float for the wheel speed and gauge speed? I'd like the stepper output to be continuous instead of discrete intervals of 1mph, but maybe I am thinking wrong in the code sense.

I'd appreciate any input. There's probably something simple that I just don't know and something will click.

#include <Stepper.h>

// steps per revolution for stepper used
const int stepsPerRevolution = 180;

// initialize the stepper library on pins 8 through 11:
Stepper myStepper(stepsPerRevolution, 8, 9, 10, 11); 

// number of steps the motor has taken (not sure if necessary?)
int stepCount = 0;  


void setup() {

  //Wheel Speed Sensor input pin
  #define WSS A0
  
  //Setting the pin mode for WSS 
  pinMode(WSS, INPUT)

}


void loop() {

  // reading the wheel speed sensor (does this need a timeout?)
  unsigned long duration = pulseIn(WSS, HIGH);
  
  // -Map a range from 0 to gauge max reading
  // -Then select an appropriate pulse count from 1 second to
  // minimum pulse time for wheel speed sensor
  
  int gaugeMax = 140
  int pulseLen = 10

  float gaugeSpeed = map(WSS, 1000000, pulseLen, 0, gaugeMax);

  // set the motor speed:

  if (WSS > 0) {

    myStepper.setSpeed(gaugeSpeed);

    // step 1/100 of a revolution:

    myStepper.step(stepsPerRevolution / 100);

  }

}

Some general observations:

  • the map() does not work with floats
  • map can work in reverse too: just flip the range to be mapped to
  • float is imprecise and a more expensive type to use than integers
  • frequency and pulse are just inverse of each other

Having said this, I would work with integers as much as possible. You don't even need float results to drive your stepper. An int in Arduino C++ is 16 bits, so you could get a resolution of 1/64k!

Some observations on the code:

  • please move #define WSS A0 to before setup()
  • map(): swap positions of gaugeMax and 0

Where is the 1000000 in the map() from?

FYI, most steppers use 200 steps per revolution.

1 Like

Thank you for your advice! As you can see, I am not exactly a skilled coder hah. I'm not sure where things go and why (like why a definition shouldn't go in the setup). I had a brain fart and wrote 180 steps for some reason, that would have been fun trouble shooting everything be off by 10%.

I recognize the frequency to time relation, I studied fourier transforms a bit in college, I just wasn't sure if it is as easy as I think it is. I created a new variable in the code to transform duration to frequency please let me know if it makes sense.

I rid the code of magic numbers. I chose the 1-E6 because that's a 1 second pulse (to my understanding pulses are measured in microseconds). I figured 1 second would be longer than any given pulse for the ground speed sensor.

What does the output of map() look like as written? If my thinking is correct, it will give me a speed which falls somewhere between 0 to 140 (or gaugeMin to gaugeMax) based on where the wheel speed sensor frequency falls in the range of specified frequencies. I'm not quite sure how 16bit translates to 1/64,000, my thinking is that it will give me an integer value in the 0-140 range. I suppose if I wanted to increase the accuracy I could go from zero to 10(gaugeMax) which would give me tenth mile per hour increments.

I still haven't touched the output speed code, but I think that's okay for now. I am also unsure of how the stepper driver fits into all of this, or if it will just play nice. I know in my other stepper experience there are acceleration and jerk settings, but if it plays off the wheel speed the acceleration should match the vehicles acceleration and it shouldn't be a problem. I might have to limit jerk to some degree, but that's another bridge to burn later.

Anyway here's the updated code. Thank you so much for taking the time to help me with this!

#include <Stepper.h>

  // steps per revolution for stepper used
  const int stepsPerRevolution = 200;

  // initialize the stepper library on pins 8 through 11:
  Stepper gaugeStepper(stepsPerRevolution, 8, 9, 10, 11); 

  // number of steps the motor has taken (not sure if necessary?)
  int stepCount = 0;  

  //Wheel Speed Sensor input pin
  #define WSS A0


void setup() {


  
  //Setting the pin mode for WSS 
  pinMode(WSS, INPUT)

}


void loop() {

  // reading the wheel speed sensor (does this need a timeout?)
  unsigned long duration = pulseIn(WSS, HIGH);

  // create a constant integer for 1 second

  const int second = 1000000
  
  // transform pulse duration to wheel speed frequency
  int WSSHz = second/duration
  
  // Input maximum and minimum reading on speedometer gauge
  // In some cases, minimum gauge reading may be below zero?
  
  int gaugeMin = 0
  int gaugeMax = 140

  // Select an appropriate pulse frequency from 0 to maximum
  // Max frequency will need to be calculated from an empirical measurement
  
  int pulseHzMin = 0
  int pulseHzMax = 100000

  // Map values to a new variable:
  
  int gaugeSpeed = map(WSSHz, pulseHzMin, pulseHzMax, gaugeMin, gaugeMax);

  // set the motor speed:

  if (WSSHz > 0) {

    gaugeStepper.setSpeed(gaugeSpeed);

    // step 1/100 of a revolution:

    gaugeStepper.step(stepsPerRevolution / 100);

  }

}

Appreciate your kind comment.

In general, in C/C++ one needs to deal with scope: meaning variables are generally only visible in the function where they are defined. Setup() is a function too, as is loop(). Something declared in setup() will not be visible in loop(). Its scope is setup().
If it needs to be visible in both functions, as well as other functions that you may create, they need to be declared global, i.e. outside of a specific function.

What would be helpful is to know how many pulses per second one would expect for each revolution of the wheel.
And whether the pulses are of fixed duration or does the duration vary with speed? It is likely to be the former.
In that case, the pulsein() function will need to be modified. The period is the sum of the high and low pulses:

unsigned long periodHigh  = pulseIn(wheel_sensor, HIGH); // Changed duration to period
unsigned long periodLow   = pulseIn(wheel_sensor, HIGH);
unsigned long periodTotal = periodHigh + periodLow;

The map function looks OK at this stage. Bit will depend on some of the above too.

Some housekeeping details: the max number that a data type can hold depends on the type: unsigned int can only hold 2^16, i.e 65,535;
int -32768 to +32767.
You will need to use long for bigger numbers

Strongly suggest using longer names for the variables, as above.

1 Like

Exactly how many turns of the speedometer cable does it take to make the odometer increase by 1 mile? You need to know this to keep the odometer accurate.

1 Like

I think I need to do some more reading/review on C/C++ structure to fully grasp this statement, or maybe just sleep and reread. Perhaps my confusion is what the setup() function is for explcitly. You can spoon feed me if you want, but I also like to figure things out. Don't worry if that's outside the realm of this discussion. I understand the gist, anything that needs to be called outside of the scope of setup will not be visible to the rest of the code.

So, the way the wheel speed sensor works is a toothed steel wheel (aka a trigger wheel) passes a magnetic pickup. When the tooth passes the magnetic pickup the circuit goes high, and when the pickup is between teeth the circuit goes low. As the wheel speed increases, the duration of the pulse will in turn decrease. I will have to count the teeth on the wheel at some point to determine how many pulses per revolution. For the sake of being general I will make a constant for triggerWheelHigh =20 and triggerWheelLow = 20 and change them as needed. Seems superflous to use both high and low pulses when one would do the job, but likely something important is not obvious to me.

My reason for going from the speed sensor to the map was so that I could do the conversion from speed sensor reading to gauge speed in another step (I still haven't changed those names [declarations, ints?] and intend to). Meaning, take the map() output and hit it with some multiplier to accomodate wheel size. That way it is more general for different vehicle applications or sensor signals.

So, I should be a little more careful using int because I don't need negative values. Does that mean I need to make

int second = 1000000

into

long unsigned int second = 1000000 ?

I might also use long just to be sure I limit the input frequency. Thanks for that tidbit.

I will take your recommendation under advisement and stop using short hand in the code. Probably something about best practices, might save me or someone else later when it comes time to change something.

In the US, the standard (from what I gathered with google) is one thousand rotations is one mile on the odometer. An equivalent statement is one mile a minute (60 mph) should also be 1000 rpm on the speedometer cable. Being that I am going to implement this (initially) in a German vehicle, I'm going to assume they set the calibration for 100 kmh (~62 mph) to be 1000 rpm on the cable. At the end of the day, this will be some multiplication factor and I will use a GPS speedometer to figure out the factor. I suppose I could skip counting the trigger wheel teeth, or including a tire size and just use a proportionality, but in honor of Germany... PRECISION. I joke, the reason I want to include those features is once it's calibrated, if I change the tire size of the vehicle I should be able to just change that value in the code with no further calibration required. It should also make it easier for other folks who decide to do this to their own car. I plan to make a YouTube video, GitHub Repo, and Thingiverse posting so other folks can make one for their own vehicle.

1 Like

Let's say the tire diameter is 26.5" and we get 20 pulses / rotation:
26.5 * PI / 12 = 6.93768377668 ft / rotation, 5280 / 6.93768377668 = 761.060920325 rotations / mile * 20 = 15221.2184065 pulses / mile.
Now a 200 step / rev motor would need 200000 steps to do 1000 rotations, so, 200000 steps / 15221.2184065 = 13.1395526073 motor steps / wheel pulse. See where I'm going? Getting the speedometer to read 60.0 MPH at exactly 1 mile / minute may not be so difficult with pulse timing, but with Arduino's ceramic resonator time base that drifts with temperature and Vcc voltage, keeping the odometer accurate within legal limits may be another story.

1 Like

As per @JCA34F above, using pulse per mile is the way to go.
Once the calibration factor has been worked out, you would multiply/divide by it to get the value to send to the stepper. It would be best to do this as the last step, since float calculations lose precision every time they are done.
You could also stick with integer arithmetic all the way through:
e.g. I want to do
10.3 * 4.6
I could do
1030 * 460 /10000

1 Like

Ah, that's probably a much simpler way of going about it. As long as the speedometer is accurate, the odometer will follow suit as they are driven off the same gear. As is, the car has larger tires than factory so the speedometer reads something like 6% lower than it should (thanks previous owner).

So, if the ceramic resonator drift is an issue (which it might or might not be) couldn't I use an external quartz resonator for better stability? Folks say the Arduino is usually accurate to within 1% which is pretty good. I don't think it would be a problem if my gauge read 60.6 mph instead of 60 mph. Realistically that's probably not even descernable. At the end of the day it means I will change my oil about 50 miles sooner. Even my newer car's speedometer is a little bit generous with it's reading at freeway speeds compared to my GPS measured speed, and it's a 2016 model year. Clearly there's a bit of tolerance for the accuracy of a speedometer and it's better to be slightly over than slightly under.

1 Like

Okay, I think I setup the code to be modular for any vehicle, speedometer revolutions per mile, steps per revolution, and final drive ratio for cable. I am not quite sure if the relationship I chose for driveStepper.setSpeed() or driveStepper.step are correct, but I believe it should give me steps per second and number of steps to take respectively.

I am also sure I need to do something about the accuracy of using int for the circumference since I am using Pi to a large decimal place. I am not exactly sure how all the math resolves, but I know @cncnotes pointed out that float calculations lose precision every time they are performed.

This became much uglier than I expected, but most of it is just relationships. I have a splitting headache at the moment but I really wanted to get this punched out so I could justify ordering an arduino, stepper, and bread board. Once again there's probably something I'm missing.

I really can't thank you enough for helping me. This is definitely the largest coding project I have ever done, I know it's relatively simple but it's still the biggest hanging point for me in this project.

Edit: Okay I am definitely missing something. Seems the order matters here because I am getting verification errors because I made a relationship with a value defined below. I'll think about it tomorrow :upside_down_face:

#include <Stepper.h>

  //----------------- Stepper Inputs --------------
  
  // define steps per revolution of stepper motor used
  const int stepsPerRevolution = 200;
  
  // initialize the stepper library on pins 8-11
  Stepper driveStepper(stepsPerRevolution, 8, 9, 10, 11);
  
    
  // initialize number of steps
  int stepCount = 0;

  // stepper output pulley tooth count
  const int stepperOutputPulley = 60;

  // cable output pulley tooth count
  const int cableOutputPulley = 8;

  
  //----------------- Vehicle Inputs --------------

  // Wheel speed sensor input pin
  #define wheelSpeedSensor A0;

  // define wheel diameter in inches (use integer math to define wheel size)
  const int wheelDiameter =  2650/100;

  // high pulses per revolution
  const int pulsePerRevolution = 20;

  // speedometer rotations per mile
  const int speedometerRotationsPerMile = 1000;

  //---------------- Stepper Relationships ------------

  // Stepper to cable drive ratio
  const int shaftFinalRatio = stepperOutputPulley/cableOutputPulley;

  // Steps per full output revolution
  const int stepPerOutputRev = stepsPerRevolution/shaftFinalRatio;

  // Stepper rotations per mile
  const int stepperRotationsPerMile = speedometerRotationsPerMile/stepPerOutputRev;

  // Steps per pulse
  const int stepsPerPulse = stepperRotationsPerMile/pulsesPerMile;

  //-------------------- Vehicle Relationships ---------------

  // inches per revolution
  const int inchesPerRevolution = PI * wheelDiameter;

  // wheel revolutions per mile
  const int wheelRevolutionsPerMile = mile/inchesPerRevolution;

  // term for pulses per mile
  const int pulsesPerMile = pulsePerRevolution*wheelRevolutionsPerMile;

  //---------------- Definitions -------------
    
  // define Pi
  #define PI 3.1415926535897932384626433832795;

  // term for one second in microseconds
  const int second = 1000000;

  // term for mile (inches)

  const int mile = 63360;


  
void setup() {
  // set pin mod for wheel speed sensor
  pinMode(wheelSpeedSensor, INPUT)

}

void loop() {
  
  // read wheel speed sensor (microseconds per pulse)
  unsigned long pulseDuration = pulseIn(wheelSpeedSensor, HIGH);

  // convert pulseDuration to frequency (pulses per second)
  int pulsePerSecond = second/pulseDuration

  if (pulsePerSecond > 0) {

    driveStepper.setSpeed(stepsPerPulse*pulsePerSecond);

    driveStepper.step(pulsePerSecond); 
  }

  

}

This becomes 26 as soon as it is evaluated, because of integer arithmetic (the part right of decimal is dropped) :upside_down_face:

float cannot hold more than 6-7 digits per Arduino ref.

I may not have been clear regarding my suggestions to use integers.
Since all your numbers are positive, I would suggest using unsigned types only
Use unsigned long data (UL) type for all the calculations
Do all the multiplication and divisions sequentially with the UL data types.
Do a division when the number would become too big for UL
Remember to append UL after each literal as well: