Instrument Cluster

In order to keep the “OEM look” of the conversion, I wanted to maintain functionality of the original instrument cluster with tachometer, speedometer, temperatures, fuel gauge, warning lights, and the driver information display. Leaving the existing hardware in place and “spoofing” sensor signals was the route taken in order to silence annoying beeps and warnings.

The crank position waveform was the hardest part to replicate in order to have the tachometer function. The waveform consists of a 58 pulse square wave at 50% duty cycle followed by a delay equivalent to the time of 2 pulses, usually referred to as a 60-2 crank signal. The original sensor and toothed ring were embedded in the drive end of the engine block for the 1.8t, however the 2.8 had the sensor mounted to the transmission bell housing and sensor ring on the flywheel. Unfortunately the 1.8t and 2.8 have different flywheel bolt patterns, so I would need to replicate the signal by other means.

Damien Maguire (Youtube channel here) had been experimenting with some Arduino code to replicate the same waveform for his BMW conversions. I’m using the same code ported over to an attiny85 with an Arduino core. The attiny85 is much nicer for this application because of its size and cost. I’m only using one input and one output and the internal 8mHz oscillator, keeping everything compact.

The protoboard sandwich:

IMG_20131210_223140

IMG_20131210_223235The code:

//Tach signal generator
#define PULSE_PIN 0
#define DEBUG_PIN 1

volatile float time = 0;
volatile float time_last = 0;
volatile int rpm_array[2] = {0,0};

int rpm = 0;
float val = 0;

void setup() {
pinMode(PULSE_PIN, OUTPUT);
//Connect directly to tach connector
pinMode(DEBUG_PIN, OUTPUT);
//Connect to led for debug
attachInterrupt(0, tach_interrupt, FALLING);
//Digital Pin 2 Set As An Interrupt
}

void triggerHigh(unsigned long val) {
digitalWrite(PULSE_PIN, HIGH);
delayMicroseconds(val);
digitalWrite(PULSE_PIN, LOW);
delayMicroseconds(val);
}

void triggerReference(unsigned long val) {
// pin should be low already
delayMicroseconds(val);
delayMicroseconds(val);
// two delays for two missing pulse.
delayMicroseconds(val);
delayMicroseconds(val);
// two delays for two missing pulse.
}

void loop(){
if(time > 0)
{
//2 Sample Moving Average To Smooth Out The Data
rpm_array[0] = rpm_array[1];
rpm_array[1] = 60*(1000000/(time*2));
//Last 2 Average RPM Counts Eqauls....
rpm = (rpm_array[0] + rpm_array[1]) / 2;
}

rpm = constrain(rpm, 500, 9000);
//analogWrite (DEBUG_PIN, rpm/36);

val = ((rpm/60)*58);
val = 1/val;
val = val/2;
val = val*1000000;

for(int i = 0; i < 58; i++)
triggerHigh(val);

triggerReference(val);
}

void tach_interrupt()
{
time = (micros() - time_last);
time_last = micros();
}

I set my Soliton jr. motor controller to output 4 pulses/turn instead of the recorded 2 which is used to track motor speed. This is intended to maximize use of the rpm scale (up to 8000rpm), giving a better sense of when you are driving the motor hard. I’m told the hard limit for an ADC 9″ is around 5000 rpm, but limiting it to 4000 won’t hurt.

The original signal through the Soliton first followed by the the attiny through some optocouplers to convert the signal to the correct waveform. This setup will indicate to me if my motor controller was not reading rpm, potentially leading to motor over speed conditions or other malfunctions.

Video of the tach working:

Beyond the OEM hardware, its also important for me to monitor my the state of charge of my batteries. Again using an Arduino with a LEM HAS DC current transducer to “count” amp hours (the sensor is only ~$30, pretty cheap for what it does while staying isolated) and display on an LCD.

My battery pack is in the trunk and I need this information relayed to the front of the car. Also, I want to measure my full pack voltage and my “half pack” voltage to check for balance or other problems. In order to keep this setup safe, the high voltage ground must be isolated from the chassis 12v system ground. To achieve isolation, I am considering a wireless sensor network approach where a master device receives data from slave nodes and displays it on an LCD. The actual LCD (2.2″ TFT, not the one shown below) will replace the Audi “Driver Information Display” seen between the tachometer and speedometer in the video above, which was a common failure item on early A4s.

IMG_20131127_194443

IMG_20131127_194403

The code is simple, it multiplies measured current over time and accumulates, approximately integrating. I was powering the whole system off a standard USB port in the photos above, which resulted in inaccurate ADC readings from the Arduino (voltage dipped due to the LCD back light). Using the analog reference pin on the Arduino or a higher capacity power supply would likely solve my problems.

The code:

//Amp hour counter
#include <LiquidCrystal.h>
#include <LcdBarGraph.h>

const float REFERENCE = 2.50; //Center voltage
float capacity = 0, current = 0, current_last = 0;
unsigned long time_now = 0, time_last = 0,
time_elapsed = 0, count=0;
int mode = -1;

LiquidCrystal lcd(8, 9, 7, 6, 5, 4);
//rs, enable, d4, d5, d6, d7
LcdBarGraph lbg(&lcd, 10, 10,3);

void setup()
{
lcd.begin(20, 4);
lcd.clear();

printHome();
}
void loop (){
count++;

printHome();

for (int i=0;i<300;i++)
readCurrent();

}

void printHome(){
//Prints static menu
if (mode !=0){
lcd.setCursor(0, 0);
lcd.print("AMPS");
lcd.setCursor(0, 1);
lcd.print("AMP HOURS");
lcd.setCursor(0, 2);
lcd.print("VOLTS");
lcd.setCursor(0, 3);
lcd.print("POWER!");
mode = 0;
}
lcd.setCursor(10, 0);
lcd.print("          ");
lcd.setCursor(10, 0);
lcd.print(current);
lcd.setCursor(10, 1);
lcd.print("          ");
lcd.setCursor(10, 1);
lcd.print(capacity);
lcd.setCursor(10, 2);
lcd.print("          ");
lcd.setCursor(10, 2);
lcd.print((analogRead(1)*5.0/1023.0));
lcd.setCursor(10, 3);
//lcd.print("          ");
lcd.setCursor(10, 3);
lbg.drawValue( abs(current), 2000);
//lcd.print(analogRead(1));
}

float readCurrent(){
current = ((analogRead(1)*5.0/1023.0) - REFERENCE)*500/0.625;
//current = constrain (current, -800, 800);
time_last = time_now;
time_now = millis();
time_elapsed = time_now - time_last;
capacity += (current + current_last)*time_elapsed/1000/60/60/2;
current_last = current;
capacity = constrain (capacity, -150, 150); //amp hours
}

Stay tuned for more EV instrumentation in part 2 including:

  • Real time clock integration
  • Datalogging
  • Multiple menus
  • Graphical LCD implementation
  • Fuel gauge integration

Part 2 can be found here.

Leave a comment