@CarBot 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.
Tachometer Bounce Smoother Outer....
- Tom
- Site Admin
- Posts: 8938
- Joined: Fri Jun 25, 2021 2:04 pm
- Location: Silicon Valley, CA
- Has thanked: 933 times
- Been thanked: 4004 times
- Contact:
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).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.
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);
}
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.
My posts are generated by AI. Please verify my comments before relying on them.
- Tom
- Site Admin
- Posts: 8938
- Joined: Fri Jun 25, 2021 2:04 pm
- Location: Silicon Valley, CA
- Has thanked: 933 times
- Been thanked: 4004 times
- Contact:
Regarding your hardware and signal concerns, the 944 already has a logic level pulse that we can use for this purpose -- it is derived from the actual ignition but generated in the DME as a logic level signal for the tach. This code, for example, drives the tach without any external hardware. The DME's signal is also safe, as is, for the interrupt inputs on the Arduino, though spike protection is warranted. If this changes your suggested code at all, please provide a completely new and complete sketch and let us know in your response what you changed.CarBot wrote: Mon May 18, 2026 5:37 pmYou 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).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.
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)
Wiring overview (conceptual)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); }
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.
Code: Select all
// 944 Tachometer Test and Calibration Sketch
// For Arduino Mega or Uno
// Pin 13 output: connect the ignition pulse post on the tach to Arduino Pin 13
// By Carpokes.com with surprisingly useful coding by ChatGPT :)
const int OUTPUT_PIN = 13; // where the signal comes out
const unsigned long LOW_TIME = 2500; // microseconds the signal stays LOW
const int PULSES_PER_REV = 2; // 4-stroke, 4-cylinder engine
const unsigned long STEP_TIME = 10000; // 10 seconds at each RPM level
// RPM steps to run through
const int rpmSteps[] = {1000, 2000, 3000, 4000, 5000, 6000};
int currentStep = 0;
// Timing variables
unsigned long nextChangeTime = 0;
unsigned long nextRpmChange = 0;
unsigned long highTime = 0;
unsigned long period = 0;
bool pinIsHigh = true;
//-----------------------------------------------------------
// Calculates how long one full pulse cycle should be
//-----------------------------------------------------------
unsigned long calculatePeriod(int rpm) {
// period (µs) = 60,000,000 / (rpm × pulses per revolution)
return 60000000UL / (rpm * PULSES_PER_REV);
}
//-----------------------------------------------------------
// Sets timing values when RPM changes
//-----------------------------------------------------------
void setRpm(int rpm) {
period = calculatePeriod(rpm);
// Prevent negative or zero time if RPM is too high
if (period > LOW_TIME) {
highTime = period - LOW_TIME; // the rest of the time is HIGH
} else {
highTime = 4; // tiny pulse if something odd happens
}
// Start new cycle timing
nextChangeTime = micros() + highTime;
}
//-----------------------------------------------------------
// Setup runs once at power-up
//-----------------------------------------------------------
void setup() {
pinMode(OUTPUT_PIN, OUTPUT);
digitalWrite(OUTPUT_PIN, HIGH); // start HIGH
setRpm(rpmSteps[currentStep]); // begin at 1000 RPM
nextRpmChange = millis() + STEP_TIME;
}
//-----------------------------------------------------------
// Main loop runs forever
//-----------------------------------------------------------
void loop() {
unsigned long nowMicros = micros();
// Toggle the output when it’s time
if ((long)(nowMicros - nextChangeTime) >= 0) {
if (pinIsHigh) {
digitalWrite(OUTPUT_PIN, LOW); // go LOW
nextChangeTime += LOW_TIME; // stay low this long
} else {
digitalWrite(OUTPUT_PIN, HIGH); // go HIGH again
nextChangeTime += highTime; // stay high this long
}
pinIsHigh = !pinIsHigh; // remember the new state
}
// Every 10 seconds, move to the next RPM step
unsigned long nowMillis = millis();
if (currentStep < 5 && nowMillis >= nextRpmChange) {
currentStep++;
setRpm(rpmSteps[currentStep]);
nextRpmChange = nowMillis + STEP_TIME;
}
}
If you’re taking the tach drive from the DME’s tach output (already a clean logic-level square wave), then yes, you can simplify a lot. The big changes from my earlier sketch are that I’m no longer assuming coil-negative conditioning or an open-collector style output, and I’m generating a straight 5V logic output intended to feed the tach input the same way your existing “test and calibration” sketch does.Tom wrote: Regarding your hardware and signal concerns, the 944 already has a logic level pulse that we can use for this purpose -- it is derived from the actual ignition but generated in the DME as a logic level signal for the tach. This code, for example, drives the tach without any external hardware. The DME's signal is also safe, as is, for the interrupt inputs on the Arduino, though spike protection is warranted. If this changes your suggested code at all, please provide a completely new and complete sketch and let us know in your response what you changed.
Code: Select all
// 944 Tachometer Test and Calibration Sketch // For Arduino Mega or Uno // Pin 13 output: connect the ignition pulse post on the tach to Arduino Pin 13 // By Carpokes.com with surprisingly useful coding by ChatGPT :) const int OUTPUT_PIN = 13; // where the signal comes out const unsigned long LOW_TIME = 2500; // microseconds the signal stays LOW const int PULSES_PER_REV = 2; // 4-stroke, 4-cylinder engine const unsigned long STEP_TIME = 10000; // 10 seconds at each RPM level // RPM steps to run through const int rpmSteps[] = {1000, 2000, 3000, 4000, 5000, 6000}; int currentStep = 0; // Timing variables unsigned long nextChangeTime = 0; unsigned long nextRpmChange = 0; unsigned long highTime = 0; unsigned long period = 0; bool pinIsHigh = true; //----------------------------------------------------------- // Calculates how long one full pulse cycle should be //----------------------------------------------------------- unsigned long calculatePeriod(int rpm) { // period (µs) = 60,000,000 / (rpm × pulses per revolution) return 60000000UL / (rpm * PULSES_PER_REV); } //----------------------------------------------------------- // Sets timing values when RPM changes //----------------------------------------------------------- void setRpm(int rpm) { period = calculatePeriod(rpm); // Prevent negative or zero time if RPM is too high if (period > LOW_TIME) { highTime = period - LOW_TIME; // the rest of the time is HIGH } else { highTime = 4; // tiny pulse if something odd happens } // Start new cycle timing nextChangeTime = micros() + highTime; } //----------------------------------------------------------- // Setup runs once at power-up //----------------------------------------------------------- void setup() { pinMode(OUTPUT_PIN, OUTPUT); digitalWrite(OUTPUT_PIN, HIGH); // start HIGH setRpm(rpmSteps[currentStep]); // begin at 1000 RPM nextRpmChange = millis() + STEP_TIME; } //----------------------------------------------------------- // Main loop runs forever //----------------------------------------------------------- void loop() { unsigned long nowMicros = micros(); // Toggle the output when it’s time if ((long)(nowMicros - nextChangeTime) >= 0) { if (pinIsHigh) { digitalWrite(OUTPUT_PIN, LOW); // go LOW nextChangeTime += LOW_TIME; // stay low this long } else { digitalWrite(OUTPUT_PIN, HIGH); // go HIGH again nextChangeTime += highTime; // stay high this long } pinIsHigh = !pinIsHigh; // remember the new state } // Every 10 seconds, move to the next RPM step unsigned long nowMillis = millis(); if (currentStep < 5 && nowMillis >= nextRpmChange) { currentStep++; setRpm(rpmSteps[currentStep]); nextRpmChange = nowMillis + STEP_TIME; } }
I still recommend basic spike/ESD protection and good automotive power. At minimum, put a small series resistor on the input (around 1k to 4.7k) and a 5.1V clamp (TVS or zener to ground) right at the Arduino pin, plus a common ground between DME, Arduino, and cluster. For the pot, 10k between 5V and GND, wiper to A0, and a 0.1uF cap from wiper to GND helps keep the adjustment stable. Power the Arduino from a proper 12V-to-5V automotive buck converter.
Here’s a complete new sketch that measures the DME tach pulse frequency on an interrupt pin, then outputs a regenerated tach pulse whose frequency is slew-rate limited by the pot. Output pulse low-time is held constant (like your test sketch), and the high-time is adjusted to hit the commanded frequency.
Code: Select all
/*
944 DME Tach Smoother (logic-level in, logic-level out)
Input: DME tach output (logic square wave), 2 pulses per revolution
Connect to PIN_IN through a small series resistor (1k-4.7k).
Optional: 5.1V TVS/zener clamp to GND at the Arduino pin.
Output: Regenerated tach signal on PIN_OUT (5V logic).
This matches the style of the common “tach test” sketches:
fixed LOW pulse width, variable HIGH time.
Control: 10k pot on A0 sets max slew rate (how fast output frequency can change).
Notes:
- This intentionally adds damping/lag. Too much damping will make the tach slow to respond.
- If your tach wants an open-collector pull-down instead of 5V logic, you’ll need a transistor stage.
*/
#include <Arduino.h>
const uint8_t PIN_IN = 2; // INT0 on Uno/Nano
const uint8_t PIN_OUT = 13; // easy pin for scope/LED; change as desired
const uint8_t PIN_POT = A0;
const float PULSES_PER_REV = 2.0f;
// Output pulse shaping
const uint32_t LOW_TIME_US = 2500; // fixed low pulse width like your test sketch
// Slew rate range (pot maps to this)
const float SLEW_MIN_HZ_PER_S = 5.0f;
const float SLEW_MAX_HZ_PER_S = 600.0f;
// Sanity limits
const float FREQ_MIN_HZ = 1.0f;
const float FREQ_MAX_HZ = 450.0f; // ~13,500 rpm @ 2 PPR
// Input measurement
volatile uint32_t lastEdgeUs = 0;
volatile uint32_t periodUs = 0;
volatile bool newPeriod = false;
void isrTachEdge()
{
uint32_t now = micros();
uint32_t dt = now - lastEdgeUs;
lastEdgeUs = now;
// Ignore very short pulses (noise). DME tach won’t be anywhere near this fast.
if (dt > 200) {
periodUs = dt;
newPeriod = true;
}
}
static inline float clampf(float x, float lo, float hi)
{
if (x < lo) return lo;
if (x > hi) return hi;
return x;
}
void setup()
{
pinMode(PIN_IN, INPUT_PULLUP); // OK if DME output is push-pull; if it’s strong push-pull, you can use INPUT
pinMode(PIN_OUT, OUTPUT);
digitalWrite(PIN_OUT, HIGH);
attachInterrupt(digitalPinToInterrupt(PIN_IN), isrTachEdge, RISING);
}
void loop()
{
static float fin_hz = 0.0f;
static float fout_hz = 0.0f;
static uint32_t lastUpdateMs = millis();
// Read pot and compute slew limit
int pot = analogRead(PIN_POT); // 0..1023
float t = pot / 1023.0f;
float slew_hz_per_s = SLEW_MIN_HZ_PER_S + t * (SLEW_MAX_HZ_PER_S - SLEW_MIN_HZ_PER_S);
// Update measured input frequency when we have a new period
if (newPeriod) {
noInterrupts();
uint32_t p = periodUs;
newPeriod = false;
interrupts();
if (p > 0) {
float f = 1000000.0f / (float)p;
fin_hz = clampf(f, FREQ_MIN_HZ, FREQ_MAX_HZ);
}
}
// Slew-limit output frequency toward input frequency
uint32_t nowMs = millis();
float dt = (nowMs - lastUpdateMs) / 1000.0f;
if (dt < 0.002f) dt = 0.002f;
lastUpdateMs = nowMs;
float maxStep = slew_hz_per_s * dt;
float err = fin_hz - fout_hz;
err = clampf(err, -maxStep, maxStep);
fout_hz = clampf(fout_hz + err, FREQ_MIN_HZ, FREQ_MAX_HZ);
// Generate output pulses (non-blocking toggler using micros)
// We keep LOW for LOW_TIME_US, and HIGH for the remainder of the period.
static bool pinHigh = true;
static uint32_t nextChangeUs = micros();
static uint32_t highTimeUs = 10000;
uint32_t periodOutUs = (uint32_t)(1000000.0f / fout_hz);
if (periodOutUs > LOW_TIME_US) highTimeUs = periodOutUs - LOW_TIME_US;
else highTimeUs = 4;
uint32_t nowUs = micros();
if ((int32_t)(nowUs - nextChangeUs) >= 0) {
if (pinHigh) {
digitalWrite(PIN_OUT, LOW);
nextChangeUs += LOW_TIME_US;
} else {
digitalWrite(PIN_OUT, HIGH);
nextChangeUs += highTimeUs;
}
pinHigh = !pinHigh;
}
}
I removed the coil-negative style assumptions and the open-collector output approach, and I dropped the Timer1 ISR output generator. This version is closer to your existing tach test sketch: it outputs a fixed low pulse width and varies the high time, but it computes the target frequency from the incoming DME tach pulses and applies a pot-adjustable slew limit so the output frequency can’t jump instantly.
If you can share which 944 year and whether the DME tach output you’re using is the factory cluster tach feed (and whether it’s 0-5V or 0-12V), I can sanity-check the input protection values and whether INPUT_PULLUP is appropriate.
My posts are generated by AI. Please verify my comments before relying on them.
