Tom wrote: we have a member with an old 944 tachometer than overshoots its target RPM, causing it to bounces around as it settles into any particular rpm. The electronics all check out, so our working theory is that the 80s-era VDO gauge is mechanically worn, causing this behavior. Rather than find a new gauge or send it out for very expensive repair, I was thinking we could write an Arduino sketch to smooth out the Tach.
The tach works on a digital pulse off the ignition signal (2 equal length on - off cycles per revolution). If we fed the input signal into an interrupt pin on the Arduino and kept track of the time between pulses, we could then output a tach pulse that matches the input with one important distinction: the output pulse width should not be allowed to change faster than a certain amount. This would ensure the tach moves up and down accurately, but not so fast that it overshoots its target and bounces around. Trial and error maybe be needed, so the rate of change limit should be easy to change for testing -- so let's assume a 10k pot is set up as a voltage divider and attached to an analog pin so that the rate of change variable is adjustable in real time. Please write an Arduino sketch to accomplish this and explain any hardware needed to support the code (e.g., schmitt trigger, pot, etc.). Please wrap the code in the code BBC.
You can do this, but a quick reality check first: most 944/early VDO tachs aren’t just “pulse in, needle out.” They usually have their own input conditioning and an internal movement driver, and the bounce can also be from noisy ignition pulses, poor grounds, or a marginal input stage. If the electronics truly check out and the movement is mechanically “ringing,” then rate-limiting the signal can calm it down, but you’re also intentionally adding lag (it won’t respond instantly to real RPM changes).
Hardware notes that matter on a 944
You do not want to connect the coil/ignition negative directly to an Arduino pin. That line has big voltage spikes and ringing.
Use an input conditioner that turns the ignition pulse into a clean 0-5V logic pulse. Easiest practical ways are an optocoupler input module made for automotive tach signals, or a transistor + zener clamp + Schmitt trigger. A Schmitt trigger (like a 74HC14 powered from 5V) after your clamp helps a lot because it cleans up slow/noisy edges so your interrupt timing doesn’t jitter.
For the output, you need to drive the tach the way it expects. Many VDO tachs want an “open collector” style pull-to-ground pulse (similar to points/coil switching), not a 5V square wave. So use a transistor output stage (2N2222/2N3904) or a logic-level MOSFET to pull the tach input to ground, with a pull-up on the tach side to switched 12V through a resistor. Add a series resistor and a clamp (zener/TVS) to keep the Arduino-side transistor safe from any kickback/noise.
Power the Arduino from a proper automotive buck converter (12V to 5V) with input transient protection. Don’t rely on the Arduino barrel jack/regulator in a car.
Pot wiring: 10k pot as a divider between 5V and GND, wiper to an analog pin (with a small cap like 0.1uF from wiper to GND if it’s noisy).
How the sketch works
It measures the incoming pulse period with an interrupt, converts that to an input frequency, then “slews” the output frequency toward the input frequency at a limited rate (adjusted by the pot). Output pulses are generated with a hardware timer interrupt so they’re stable.
Assumptions you gave: 2 pulses per revolution. You can change that constant if needed.
Code (wrap in code BBC)
Code: Select all
/*
944 Tach Smoother / Slew-Rate Limited Tach Pulse Regenerator
- Measures input pulse frequency on INT0 (D2) using micros() timestamps.
- Generates output pulses on D9 (OC1A) using Timer1 compare interrupt.
- Output frequency follows input frequency but is limited in how fast it can change.
- Slew rate limit is adjustable with a 10k pot on A0.
IMPORTANT:
- Do NOT connect coil negative directly to Arduino.
- Use proper input conditioning (opto or clamp + Schmitt trigger).
- Use a transistor/MOSFET output stage to drive the tach input (open-collector style).
Assumptions:
- 2 pulses per revolution (PPR = 2).
*/
#include <Arduino.h>
const uint8_t PIN_IN = 2; // INT0
const uint8_t PIN_OUT = 9; // OC1A pin on Uno/Nano (we'll toggle it in ISR)
const uint8_t PIN_POT = A0;
const float PPR = 2.0f; // pulses per revolution
// Input measurement
volatile uint32_t lastEdgeUs = 0;
volatile uint32_t periodUs = 0;
volatile bool newPeriod = false;
// Output generation state
volatile bool outState = false;
// Filter/slew state
float fin_hz = 0.0f; // measured input frequency (Hz)
float fout_hz = 0.0f; // commanded output frequency (Hz)
// Timing
uint32_t lastUpdateMs = 0;
// Safety limits
const float FREQ_MIN_HZ = 1.0f; // avoid divide-by-zero and silly low values
const float FREQ_MAX_HZ = 400.0f; // 400 Hz = 12,000 RPM at 2 PPR ( (12000/60)*2 = 400 )
// Output pulse width control
// Many tachs are happy with ~2-5 ms low pulse. We'll do 3 ms low, rest high.
// If your tach expects different, adjust.
const uint16_t PULSE_LOW_US = 3000;
// Slew rate mapping (pot -> max Hz change per second)
const float SLEW_MIN_HZ_PER_S = 5.0f; // very smooth/slow
const float SLEW_MAX_HZ_PER_S = 500.0f; // very responsive
void isrInputEdge()
{
uint32_t now = micros();
uint32_t dt = now - lastEdgeUs;
lastEdgeUs = now;
// Basic sanity: ignore extremely short periods (noise)
if (dt > 200) { // >200us => <5000 Hz, well above any tach need
periodUs = dt;
newPeriod = true;
}
}
// Configure Timer1 to fire an interrupt at a programmable interval.
// We'll implement a simple state machine: low pulse for PULSE_LOW_US, then high for the remainder of the period.
volatile uint32_t outPeriodUs = 50000; // default 20 Hz
volatile uint32_t nextIntervalUs = 10000;
ISR(TIMER1_COMPA_vect)
{
// Toggle output state machine
if (!outState) {
// Go LOW for fixed pulse width
outState = true;
digitalWrite(PIN_OUT, LOW);
nextIntervalUs = PULSE_LOW_US;
} else {
// Go HIGH for the remainder of the period
outState = false;
digitalWrite(PIN_OUT, HIGH);
uint32_t highTime = 0;
if (outPeriodUs > PULSE_LOW_US) highTime = outPeriodUs - PULSE_LOW_US;
else highTime = 100; // minimum high time
nextIntervalUs = highTime;
}
// Schedule next compare
// Timer1 runs at 16 MHz / 8 = 2 MHz => 0.5 us per tick
// OCR1A is in ticks, so ticks = us * 2
uint16_t ticks = (uint16_t)min((uint32_t)60000, nextIntervalUs * 2UL); // cap to avoid overflow
OCR1A = ticks;
TCNT1 = 0;
}
void setupTimer1()
{
pinMode(PIN_OUT, OUTPUT);
digitalWrite(PIN_OUT, HIGH); // idle high (open-collector style will invert via transistor if needed)
noInterrupts();
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
// CTC mode
TCCR1B |= (1 << WGM12);
// Prescaler /8 => 2 MHz timer clock
TCCR1B |= (1 << CS11);
// Initial compare
OCR1A = 20000; // 10,000 us * 2 ticks/us
TIMSK1 |= (1 << OCIE1A);
interrupts();
}
void setup()
{
pinMode(PIN_IN, INPUT_PULLUP); // depends on your conditioner; OK for open-collector opto outputs
attachInterrupt(digitalPinToInterrupt(PIN_IN), isrInputEdge, RISING);
setupTimer1();
lastUpdateMs = millis();
}
void loop()
{
// Read pot and compute slew limit
int pot = analogRead(PIN_POT); // 0..1023
float alpha = pot / 1023.0f;
float slew_hz_per_s = SLEW_MIN_HZ_PER_S + alpha * (SLEW_MAX_HZ_PER_S - SLEW_MIN_HZ_PER_S);
// Update input frequency if we have a new period
if (newPeriod) {
noInterrupts();
uint32_t p = periodUs;
newPeriod = false;
interrupts();
if (p > 0) {
float f = 1000000.0f / (float)p;
// Clamp to sane range
if (f < FREQ_MIN_HZ) f = FREQ_MIN_HZ;
if (f > FREQ_MAX_HZ) f = FREQ_MAX_HZ;
fin_hz = f;
}
}
// Slew-limit output frequency toward input frequency
uint32_t nowMs = millis();
float dt = (nowMs - lastUpdateMs) / 1000.0f;
if (dt < 0.005f) dt = 0.005f; // avoid tiny dt
lastUpdateMs = nowMs;
float maxStep = slew_hz_per_s * dt;
float err = fin_hz - fout_hz;
if (err > maxStep) err = maxStep;
if (err < -maxStep) err = -maxStep;
fout_hz += err;
// Convert fout_hz to period and update the ISR period atomically
if (fout_hz < FREQ_MIN_HZ) fout_hz = FREQ_MIN_HZ;
if (fout_hz > FREQ_MAX_HZ) fout_hz = FREQ_MAX_HZ;
uint32_t newPeriodUs = (uint32_t)(1000000.0f / fout_hz);
noInterrupts();
outPeriodUs = newPeriodUs;
interrupts();
// Small delay to reduce loop jitter
delay(5);
}
Wiring overview (conceptual)
Input: ignition/tach source into an automotive-safe conditioner, then into D2. If you use an optocoupler module with open-collector output, the Arduino INPUT_PULLUP is fine. If you use a Schmitt trigger, feed it 5V and output directly to D2.
Output: D9 into a transistor driver that pulls the car’s tach input to ground. Typically you’d have the tach input pulled up to switched 12V through something like 1k to 10k, then the transistor sinks it. If the tach expects the opposite polarity, you can invert in software by swapping HIGH/LOW behavior, but most “points style” inputs like a pull-down pulse.
If you tell me the exact year 944 and whether the tach input is coming from coil negative, DME, or an aftermarket ignition box, I can suggest a more specific input/output conditioning approach that won’t cook the Arduino or upset the DME.