Read, modify and output pulse - VSS Signal modification

Hi there,

I'm currently building a Mini Cooper with an engine swap. I need to get the VSS signal right for the ECU and speed gauge. The correction is necessary due to different tire dimensions.

I know there are existing products out there, but I was ambitious that I could build something myself.

So this is the information I got:

  • VSS signal is a 0-5V rectangular signal with 50% duty cycle
  • 4 Pulses per wheel turn

So I did some calculations to get an idea about the timing:

Figures for 1KPH

Car Tire circumferences(m) Wheel turns per second Pulses per second us/pulse
Donor 1,792 0,155009... 0,620039... 1612800
Mini 1,536 0,180844... 0,723379... 1382400

Figures for 200KPH

Car Tire circumferences(m) Wheel turns per second Pulses per second us/pulse
Donor 1,792 31,0019... 124,0079... 8064
Mini 1,536 36,1689... 144,6759... 6912

There might be some calculation errors, but anyway - this gives an idea about the dimensions for the pulse width. So based on what I've read about the precision, this should generally be possible.

However, I can't get my head around what's the best strategy to read a pulse at an input pin and do some kind of "on-the-fly" conversion and forward the newly calculated signal to a dedicated output pin.

I roughly calculated that I need to extend the pulse by 16% - so this is a sketch I came up with - not tested yet since the car is currently not in running condition.

My idea is to use interrupts to detect two rising edges - since I know that it's a 50% duty cycle, I will calculate the high time at a later stage. So as soon as I have the complete pulse length, I stop the interrupts and generate a 50% duty-cycle signal that is extended by the mention 16%. This should "lower" the speed signal and give me more correct values.

Following things I'm aware of but couldn't come up with an idea to solve:

  • detecting and correctly implementing the potential micros() rollover after some time
  • evaluating the impact that i will loose track of the next pulse while I'm generating the modified pulse and sending it to the output
// all those variables

volatile float timeNow = 0;
volatile float timeOld = 0;
byte pinInterrupt = 2;
byte pinPulse = 6;
float valCorrection = 1.166666;
float pulseOld = 0;
float pulseNew = 0;
float halfPulse = 0;

void setup() {
  // put your setup code here, to run once:
  attachInterrupt(digitalPinToInterrupt(pinInterrupt), recTime, RISING);
  pinMode(pinPulse, OUTPUT);
  digitalWrite(pinPulse, LOW);
}

void loop() {
  // put your main code here, to run repeatedly:
  if ((timeOld > 0) && (timeNow > timeOld)) {
    // stop listening for interrupts to do calculation and singal generation
    // detachInterrupt(digitalPinToInterrupt(pinInterrupt)); 
    noInterrupts();
    pulseOld = timeNow - timeOld;  // calculate the actual pulse (RISE to next RISE)
    pulseNew = valCorrection * pulseOld;  // adapt the actual pulse to the target (with correction)
    halfPulse = pulseNew/2.0;  // calculate duration for 50% duty signal
    digitalWrite(pinPulse, HIGH);
    delayMicroseconds((int)halfPulse); // keep pin HIGH for the duty cycle
    digitalWrite(pinPulse, LOW);
    delayMicroseconds((int)halfPulse); // keep pin low to complete the "pulse"

    // reattach interrupt to continue recording, hopefully should not miss a signal at all
    //attachInterrupt(digitalPinToInterrupt(pinInterrupt), recTime, RISING);  
    interrupts();
  }
}

void recTime() {
  timeOld = timeNow;
  timeNow = micros();
}

Did some do something comparable and maybe give me some hints that I maybe started totally wrong at the beginning?

Thank you!

maybe give me some hints that I maybe started totally wrong at the beginning?

//volatile float timeNow = 0;
//volatile float timeOld = 0;
volatile unsigned long timeNow = 0;
volatile unsigned long timeOld = 0;

These are microsecond values and should be typed as volatile unsigned long. The unsigned long type also ensures that subtraction of timeOld from timeNow will have no rollover issues.

The other time values representing microseconds should also be typed as unsigned long and not floats.

//float pulseOld = 0;
//float pulseNew = 0;
//float halfPulse = 0;
unsigned long pulseOld = 0;
unsigned long pulseNew = 0;
unsigned long halfPulse = 0;
 if ((timeOld > 0) && (timeNow > timeOld)) {

I think that reading a new pulse should be triggered by setting a newPulse flag variable in the isr and testing for it in loop. Set it false after reading. Since the variable is a a byte, it does not need a protected reading.

The noInterrupts() protected transfer of data should be as short as possible and all calculations and output should be outside of the transfer.

noInterrupts();
pulseOld = timeNow - timeOld;  // calculate the actual pulse (RISE to next RISE)
interrupts();

You are going to need a hardware or software timer to replace the use of delayMicroseconds() for the output pulse.

    digitalWrite(pinPulse, HIGH);
    delayMicroseconds((int)halfPulse); // keep pin HIGH for the duty cycle
    digitalWrite(pinPulse, LOW);
    delayMicroseconds((int)halfPulse); // keep pin low to complete the "pulse"

not tested yet since the car is currently not in running condition.

You will need a way to test the code without the car. Without a signal generator and scope, it will be best to use a second Arduino to generate and test pulses and read the response. You may find it possible to do all the testing on one Arduino, but that will be more difficult.

There have been previous postings on this very topic. I would be good for you to do an extensive google search.

Thank's a lot for that extensive answer, really appreciate it!
I have several Arduinos on hand - so might be not too difficult to generate a reference signal to verify my readings and outputs. Maybe I can also use a spare speedometer (exact same that will be used within the car) I've left over to verify the readings "in real life".

cattledog:
There have been previous postings on this very topic. I would be good for you to do an extensive google search.

Most of the examples I've found so far are "just" about reading or writing pulses - not manipulating and outputting them. However, I may need to spend some more time in searching - anyway, thank's a lot!

How does a Mini Cooper service shop do it? I would think a technician would just enter a new number into the car's computer via the OBD connector.

I’d shoot the engineer that named an active ‘signal’ Vss !

That is typically reserved for the substrate supply rail - usually 0V

I'd shoot the engineer that named an active 'signal' Vss !

Look again its not Vss its VSS = Vehicle Speed Sensor.
No need to threaten any engineers!

JCA79B:
How does a Mini Cooper service shop do it? I would think a technician would just enter a new number into the car's computer via the OBD connector.

I left some details unclear - it's one of the 'old' Mini Cooper (not the BMW). The used engine is from a Honda from 1995 with OBD1 - not possible (at least for me) to manipulate using an OBD1 interface.

And yes, maybe I should have explicitly written "Vehicle Speed Sensor signal" - I keep forgetting that there are people around here that have some background in electronics :smiley:

However, a friend came up with following solution - seems quite smart (from what I understand). But I have to go again into detail about the variables. I guess cattledog gave some good hints:

The basic idea: do not stop the interrupt but accept that there might be some inaccuracies if a pulse get's detected during the output pulse generation. Maybe that's a good point to check again "blink without delay".

Alternatively, there might also be a possible solution going the "hardware way" and check possibilities of a "binary rate multiplier" (something I found while using google - I guess that's also the way most of the commercial "speedo healers" are build). But I guess this exceeds my skills a lot - so I'll give the arduino/software approach a try.

/*

*/
volatile float pulse = 0;
volatile float timeOld = 0;
volatile float timeNow = 0;

const float correctionVal = 1.234;

byte pinInterrupt = 2;
byte pinPulse = 6;



void setup() {
    attachInterrupt(digitalPinToInterrupt(pinInterrupt), interruptPulse, RISING);
    pinMode(pinPulse, OUTPUT);
    digitalWrite(pinPulse,LOW);
}

void loop() {
    if(pulse >0){
      // buffer to ensure pulse not changed during one cycle
      int pulseBuffer = (int)((pulse*correctionVal) / 2.0);
      digitalWrite(pinPulse, HIGH);
      delayMicroseconds(pulseBuffer);
      digitalWrite(pinPulse,LOW);
      delayMicroseconds(pulseBuffer);
    }
    else
    {
      // do whatever
      digitalWrite(pinPulse,LOW);
    }
}

void interruptPulse() {
  timeOld = timeNow;
  timeNow = micros();
  // workaround for micros overflow
  // on overflow 1 pulse skipped
  if(timeNow > timeOld)
  {
    pulse = (timeNow - timeOld) ;
  }
}
// workaround for micros overflow
  // on overflow 1 pulse skipped
  if(timeNow > timeOld)
  {
    pulse = (timeNow - timeOld) ;
  }

This is fundamentally wrong. Use subtraction with unsigned long variables. See https://www.gammon.com.au/millis

The basic idea: do not stop the interrupt but accept that there might be some inaccuracies if a pulse get's detected during the output pulse generation. Maybe that's a good point to check again "blink without delay"

Blink without delay is going to be required. I'm not sure that "pulseBuffer" will do what you think. With the output pulse longer than the input pulse you are bound to have two measurements of input pulse length during an output pulse at some point, and I think you will use the last measured value with either 'pulse' or "pulseBuffer".

Rather than working on a pulse to pulse basis, you may want to use some smoothing or averaging. For example, you could base base the output pulse length on a moving average of the last three input pulse lengths. Responsiveness to change a very low speeds may be an issue, but you are driving a Mini Cooper and not a tractor. At less than 5 kph you may not really care.

cattledog:
This is fundamentally wrong. Use subtraction with unsigned long variables. See https://www.gammon.com.au/millis

Blink without delay is going to be required. I'm not sure that "pulseBuffer" will do what you think. With the output pulse longer than the input pulse you are bound to have two measurements of input pulse length during an output pulse at some point, and I think you will use the last measured value with either 'pulse' or "pulseBuffer".

Rather than working on a pulse to pulse basis, you may want to use some smoothing or averaging. For example, you could base base the output pulse length on a moving average of the last three input pulse lengths. Responsiveness to change a very low speeds may be an issue, but you are driving a Mini Cooper and not a tractor. At less than 5 kph you may not really care.

Thank you, that was valuable input - especially the article was written quite nice.
So, not sure if really understand all your suggestions, anyway - this is my latest idea, tried to pick-up all your remarks. Code hasn't been tested yet, just a rough idea. I guess it can be cleaned a lot and I'm doing some extra work that's not necessary or might even cause some errors - we will see.

#include "RunningAverage.h"
// initialize RunningAverage
RunningAverage myRA(4);

// define pins
const byte pinInterrupt = 2;
const byte pinPulse = 6;

// define volatile variables for interrupt handling
volatile unsigned long timeNow = 0;
volatile unsigned long timeOld = 0;
volatile unsigned long pulse = 0;
volatile boolean interruptFired = false;

// define some more timing variables
unsigned long currentMicros = 0;
unsigned long previousMicros = 0;
unsigned long bufferInterval = 0;

// some control variables
boolean recalcBuffer = true;
byte counter = 0;
int pinPulseState = LOW;

// correction value
const float corrVal = 1.18;

void setup() {
  myRA.clear(); // make sure the running average is empty
  // setup pins and interrupts
  pinMode(pinPulse, OUTPUT);
  digitalWrite(pinPulse, pinPulseState);
  attachInterrupt(digitalPinToInterrupt(pinInterrupt), interruptPulse, RISING);
}

void loop() {
  // update RunningAverage if interrupt was fired
  if (interruptFired) {
    myRA.addValue(pulse);
    interruptFired = false;
  }
  // only run any code below if there is a value available
  if (myRA.getAverage() > 0) {
    /* if necessary, update the interval that should be completed
    should only be updated after a pulse has been completely sent
    */
    if (recalcBuffer) {
      bufferInterval = (long)(myRA.getAverage()*corrVal/2.0);
      recalcBuffer = false; // stop recalculating in next loop
    }
  
    // now all I'm doing is trying to adapt BlinkWithoutDelay
    unsigned long currentMicros = micros();
  
    if (currentMicros - previousMicros >= bufferInterval) {
      // save the last time I changed the pinPulse state
      previousMicros = currentMicros;
  
      // if the pinPulse is LOW turn it HIGH and vice-versa:
      if (pinPulseState == LOW) {
        pinPulseState = HIGH;
      } else {
        pinPulseState = LOW;
      }
  
      // set the pinPulse with the pinPulseState of the variable:
      digitalWrite(pinPulse, pinPulseState);
      // increment counter
      counter = counter +1;
      // if this is the 3rd time running, the pulse should get updated
      // to have the correct interval within the next loop
      if (counter > 2) {
        counter = 0;
        recalcBuffer = true;
      }
    }
  }
}

// this interrupt function records the time between it was triggered again
void interruptPulse() {
  timeOld = timeNow;
  timeNow = micros();
  pulse = timeNow - timeOld ;
  interruptFired = true;
}