Christians Webseite        << zurueck        vor >>

Eine Bauanleitung für eine digitale Tankanzeige

Die folgende Beschreibung ist eine Anleitung zum Nachbau einer digitalen Tankanzeige.
Sie ist gedacht als Ersatz für die simplen und ungenauen Tankuhren in Motorrädern.

tankanzeige
Durch die Elektronik und Software ist es möglich alle Ungenauigkeiten (durch Tankgeber und Tankform) zu kompensieren und eine Liter-genaue Anzeige zu erreichen.

Diese Tankanzeige ist NICHT für Tanks geeignet die nur einen NTC und eine Leer-Warnlampe haben.
(Aber auch in solchen Fällen sollte was möglich sein. Nur mal als Idee: wenn man mehrere NTC im Tank befestigt könnte man eine 3 oder 4 stufige Anzeige realisieren..)

Bauteile

Die elektronische Basis ist ein Arduino-Nano-Board.
arduino arduino

Das Display ist ein sog. statisches LCD DE112 mit 2 Dezimalstellen.
LCD Display LCD Display LCD Display

Der Arduino ist auf der Unterseite einer Lochpunktrasterplatte befestigt. Auf der Oberseite sitzt das Display und die elektronischen Bauteile.
tankanzeige tankanzeige tankanzeige

Schaltplan

Im Schaltplan sind die zusätzlichen elektronischen Bauteile zu erkennen.
Die Verdrahtung am Motorad ist sehr simpel.
tankanzeige tankanzeige
- Die Stromversorgung wird gefiltert und gegen Überspannungen geschützt (D1, R1, R3, C24).
- Vor den beiden Messeingängen (A7 und A6) befinden sich Schutz- und Anpasschaltungen.

Der Widerstand R26 (150 Ohm, 3 W) liefert den Strom für den Tanksensor. Er kann relativ warm werden und ist deshalb eine "fette", bedrahtete Type.

Beschreibung

Die übliche Konfiguration einer Tankanzeige besteht aus einem Tanksensor (oder Tankgeber) und einem Anzeigeinstrument.
tankanzeige tankanzeige
tankanzeige
Der Tanksensor verändert seinen Widerstand mit dem Füllstand. Das Anzeigeinstrument misst wieviel Strom durch den Sensor fliesst. Die aufgedruckte Skala macht die "Umrechnung" in Füllstand.

In der digitalen LCD-Variante entfällt das Anzeigeinstrument und wird durch einen 150 Ohm Widerstand ersetzt.
tankanzeige
Die Spannung an den beiden Messpunkten "12V" und "Sensor" wird vom Arduino gelesen. Aus diesen Werten wird der aktuelle Widerstand des Tanksensors berechnet. Mit Hilfe einer Korrekturtabelle wird daraus der aktuelle Füllstand berechnet und angezeigt.
Die Messung der "12V"-Versorgung erscheint auf den ersten Blick unsinnig, in der Realität schwanken die "12V" aber stark.

Hier ein Beispiel vom Auslitern einer realen Tank-Kennlinie (Yamaha FZS1000).
tankanzeige
Man erkennt sofort dass die Kennlinie alles andere als linear verläuft. Ausserdem liefert der Sensor insgesamt nur 13 diskrete Werte. Dies ist heuzutage durchaus üblich, und auch ausreichend. Lineare Potis werden selten eingesetzt. Vermutlich sind die gestuften Schalter zuverlässiger.

Software

Die Charakteristik des Tank ist als Tabelle abgelegt. 10 Wertepaare sind ausreichend, jeweils Liter- und Ohm-Wert. Zur Vereinfachung ist der Zahlenbereich auf Integer reduziert.

 
uint16_t TanktabelleLiter[10] = {  0,  3,  4,  7, 11, 13, 16, 18, 20, 22};
uint16_t TanktabelleOhm[10]   = { 86, 80, 70, 48, 39, 35, 31, 24, 20,  0};

In diesem Beispiel hat der Tanksensor einen Widerstand von 86 Ohm bei 0 Liter Füllstand, und 0 Ohm bei 22 Liter.

Zu Beginn des Programms werden die Min- und Max-Werte aus der Tabelle bestimmt.
Der Programmablauf in Kurzform:
- Die Funktionen des Prozessors aktivieren (Ports, UART, ADC und Timer).
- Variablen vorbesetzen (z.B Skalierungsfaktoren).
- Die Tanktabelle nach Min-/ Max-Werten durchsuchen.
- Die Berechnung des gleitenden Mittelwerts vorbesetzen.
- Loop Anfang
-- Spannungen einlesen und TankSensorWiderstand berechnen.
-- Füllstand berechnen und Mittelwert bilden.
-- Messwerte per Schnitstelle ausgeben.
-- Füllstand auf LCD ausgeben.
- Loop Ende

Die Ausgabe der Messwerte über die serielle Schnitstelle dient nur zum debuggen.
(Was noch fehlt ist die Möglichkeit die Einstellwerte und die Tankkennlinie über die Schnittstelle zu konfigurieren.)

Die Kernfunktionen sind die folgenden (Beschreibungen weiter unten):
- TanksensorWiderstandLesen(void)
- TankFuellstandAktuell(void)
- TankFuellstandMittelwert(void)
- ShowZiffer(cText)

Zur Berechnung des aktuellen Widerstands des Tankgebers wird dessen Spannung gelesen. Zusätzlich die Batteriespannung um Schwankungen herauszurechnen.

 
float TanksensorWiderstandLesen(void)
{	// Tanksensor Ohm-Wert bestimmen
	float fVbatt, fVFuel, fR_x, fR_x_real;

	fR_x = 0;	// Rückgabewert, wird später auf integer begrenzt
	fR_x_real = 0;	// für interne Berechung

	fVFuel = ((float)ADC_readchannel(ADCinputTanksensor)*VoltageDividerFuel);
		// Messignal, korrigiert um Spannungsteiler-Faktor
	fVbatt = ((float)ADC_readchannel(ADCinput12V)*VoltageDividerBatterie);
		// Batteriespannung, korrigiert um Spannungsteiler-Faktor

	if (fVFuel > 0.01) {	// Messpannung vorhanden und im Range?
		if ( ((fVbatt/fVFuel)-1.0) > 0){ // Berechung gültig? Division durch 0 verhindern
			fR_x_real = (WorkResistorFuel/((fVbatt/fVFuel)-1.0));	// Sensorwiderstand berechnen
		}
	}

	fR_x = trunc(fR_x_real);	// Messwert auf integer begrenzen
  
	if (fR_x < FuelSensorMinResistance) fR_x = FuelSensorMinResistance;
		// Zahlenbereiche begrenzen
	if (fR_x > FuelSensorMaxResistance) fR_x = FuelSensorMaxResistance;
		// Grenzwerte wurden beim Einlesen der Tank-Tabelle bestimmt

	return fR_x;
}


Mit dem Widerstandswert werden in der Tabelle die beiden nächsten Ohm-Punkte gesucht. Die zugehörigen Liter-Werte werden für eine Interpolation benutzt. Das Ergebnis ist der aktuelle Füllstand.

 
float TankFuellstandAktuell(void)
{	// Tanktabelle nach passendem Ohm-Wert durchsuchen und Liter-Wert interpolieren
	// Liter-Wert aus den beiden nächsten Ohm-Werten interpolieren	

	int8_t Tanki, Tankmerk1, Tankmerk2;	// Hilfsvaraiablen
	float OhmWert, OhmAbweichung1, OhmAbweichung2;
	float TankSuchHilf;
	float InterpolDelta1, InterpolDelta2, InterpolDelta3, LiterInterpoliert;

	LiterInterpoliert = 0;	// Interpolierter Füllstand, Default 0
	OhmWert = TanksensorWiderstandLesen();	
	// Tanksensor-Spannung lesen und Ohmwert bestimmen, Wertebereich ist auf Tabellenwerte beschränkt

	Tankmerk1 = -99;	// Hilfs-Indexe fürs Durchsuchen der Tabelle
	Tankmerk2 = -99;	// Default auf -99 setzen, zur späteren Erkennung ob er gültig ist

	OhmAbweichung1 = 10000;	// Default-Werte für die Berechung der Differenzen
	OhmAbweichung2 = -10000;												
		
	for (Tanki=0; Tanki<10; Tanki++){	// Tank-Tabelle durchsuchen
		TankSuchHilf = TanktabelleOhm[Tanki];	
		// Wert aus Tank-Tabelle in Hilfsvariable kopieren weil später negative Werte möglich
		TankSuchHilf = (TankSuchHilf - OhmWert);	// Abweichung berechnen, Tabellenwert - Messwert
		if ((TankSuchHilf >= 0)&&(TankSuchHilf <= OhmAbweichung1)){	
		// Indexwert mit kleinster positiver Abweichung finden, Messwert ist kleiner/gleich als Tabellenwert 
			Tankmerk1=Tanki;
			OhmAbweichung1 = TankSuchHilf;
		}
		if ((TankSuchHilf < 0)&&(TankSuchHilf > OhmAbweichung2)){	
		// Indexwert mit kleinster negativer Abweichung finden, Messwert ist grösser als Tabellenwert
			Tankmerk2=Tanki;
			OhmAbweichung2 = TankSuchHilf;
		}
	} 

	if ((Tankmerk1 != -99)&&(Tankmerk2 != -99)){	
	// normaler Fall, beide Indexe gefunden -> normale Interpolation	
		InterpolDelta1 = ((float)TanktabelleOhm[Tankmerk1]-(float)TanktabelleOhm[Tankmerk2]);
		InterpolDelta2 = ((float)TanktabelleLiter[Tankmerk1]-(float)TanktabelleLiter[Tankmerk2]);
		InterpolDelta3 = ((float)TanktabelleOhm[Tankmerk1]-OhmWert);
		
		if (fabsf(InterpolDelta1) < 0.01) {	// zu kleine Abweichungen ignorieren
			LiterInterpoliert = (float)(TanktabelleLiter[Tankmerk1]);
		} else {	// normaler Fall
			LiterInterpoliert = (float)(TanktabelleLiter[Tankmerk1]) 
			 - InterpolDelta3/InterpolDelta1*InterpolDelta2;	// lineare Interpolation
		}
	} else {
		if (Tankmerk1 != -99) {	// nur Index 1 wurde gefunden, alle Tabellenwerte >= gemessenem Ohmwert
			LiterInterpoliert = (float)(TanktabelleLiter[Tankmerk1]);
			// letzter gefundener Literwert wird benutzt
		}
		if (Tankmerk2 != -99) {	// nur Index 2 wurde gefunden, alle Tabellenwerte < gemessenem Ohmwert 
			LiterInterpoliert = (float)(TanktabelleLiter[Tankmerk2]);	
			// erster gefundener Literwert wird benutzt
		}
	}

	if (LiterInterpoliert < TankMinimum) LiterInterpoliert = TankMinimum;	// Werte begrenzen
	if (LiterInterpoliert > TankMaximum) LiterInterpoliert = TankMaximum;

	return LiterInterpoliert;
}



Der aktuelle Füllstandswert schwankt sehr stark und muss gemittelt werden. Dazu werden fortlaufend die jeweils 16 zurückliegenden Messwerte in einem Array gespeichert (RingBufferTank) und deren Mittelwert berechnet. Ein neuer Wert wird nur hinzugefügt wenn die Wartezeit abgelaufen ist (LiterMittelwertDelay, empirisch festgelegt). Diese simple Methode kommt ohne zusätzlichen Timer aus.

 
float TankFuellstandMittelwert(void)
{	// TankFüllstand abfragen und Mittelung über Zwischenspeicher (16x)
	float fliter;
	uint8_t l_index;
	
	if (TankMessungIndex >= LiterMittelwertDelay){
		// nur Messwerte einlesen wenn Wartezeit zwischen den Messungen abgelaufen ist
		TankMessungIndex =0;

		RingBufferTank[RingBufferTankIndex] = TankFuellstandAktuell();	// neuen Wert lesen	
		RingBufferTankIndex++;
		RingBufferTankIndex=(RingBufferTankIndex&15);	// Indexbegrenzung auf 0..15
	} else {
		TankMessungIndex++;	
		// Wartezeit noch nicht abgelaufen, Zähler inkrementieren, keinen neuen Wert hinzufügen
	}

	RingBufferTankSumme = 0;								
	for (l_index=0; l_index<16; l_index++){	// Summe aller Werte berechnen
		RingBufferTankSumme += RingBufferTank[l_index];
	}

	fliter = (float)(RingBufferTankSumme ) / 16.0	// Mittelwert bilden

	if (fliter < TankMinimum) fliter = TankMinimum;	// Werte begrenzen
	if (fliter > TankMaximum) fliter = TankMaximum;
	return fliter;
}



Der Füllstands-Mittelwert wird als "charakter" an die Anzeigeroutine übergeben. Dort wird werden die passenden Segmente des LCD gesetzt.

 
void ShowZiffer(char *Text)
{	// Segmente in Variable SEG (= Segment) kodieren

	uint8_t Zeichen1;
	uint8_t Zeichen2;

	Zeichen1 = 0;
	Zeichen2 = 0;
	if (Text[0] != 0) {			// erstes Zeichen vorhanden
		Zeichen1 = Text[0];
		if (Text[1] != 0) {		// zweites Zeichen vorhanden
			Zeichen2 = Text[1];	
		}
	}
	switch(Zeichen1){				
		case 1  : SEG1 = 0b01001001; break;  //A ;D ;G ; 		// Seg A+G+D
		case 2  : SEG1 = 0b00110110; break;  //B ;C ;E ;F ;		// Sonderzeichen
		case 3  : SEG1 = 0b00001001; break;  //A ;D ; 			// Seg A+D
		case 4  : SEG1 = 0b00011011; break;  //A ;B ;E ;D ; 		// Seg A+B+D+E
		case 5  : SEG1 = 0b00101101; break;  //F ;A ;C ;D ; 		// Seg F+A+C+D
		case 6  : SEG1 = 0b00110111; break;  //A ;B ;C ;E ;F ; 		// Seg A+B+C+E+F = spezielles "N"
		case 32 : SEG1 = 0b00000000; break;  //				// Blank
		case 45 : SEG1 = 0b01000000; break;  //G ; 			// "-"
		case 48 : SEG1 = 0b00111111; break;  //A ;B ;C ;D ;E ;F ; 	// "0"
		case 49 : SEG1 = 0b00000110; break;  //B ;C ; 			// "1"
		case 50 : SEG1 = 0b01011011; break;  //A ;B ;D ;E ;G ; 		// "2"	
		case 51 : SEG1 = 0b01001111; break;  //A ;B ;C ;D ;G ; 		// "3"
		case 52 : SEG1 = 0b01100110; break;  //B ;C ;F ;G ; 		// "4"
		case 53 : SEG1 = 0b01101101; break;  //A ;C ;D ;F ;G ; 		// "5"
		case 54 : SEG1 = 0b01111101; break;  //A ;C ;D ;E ;F ;G ; 	// "6"
		case 55 : SEG1 = 0b00000111; break;  //A ;B ;C ; 		// "7"
		case 56 : SEG1 = 0b01111111; break;  //A ;B ;C ;D ;E ;F ;G ; 	// "8"
		case 57 : SEG1 = 0b01101111; break;  //A ;B ;C ;D ;F ;G ;	// "9"
		case 61 : SEG1 = 0b01001000; break;  //D ;G ; 			// "="
		case 65 : SEG1 = 0b01110111; break;  //A ;B ;C ;E ;F ;G ;	// "A"
		case 69 : SEG1 = 0b01111001; break;  //A ;D ;E ;F ;G ; 		// "E"
		case 70 : SEG1 = 0b01110001; break;  //A ;E ;F ;G ; 		// "F"
		case 76 : SEG1 = 0b00111000; break;  //D ;E ;F ; 		// "L"
		case 83 : SEG1 = 0b01101101; break;  //A ;C ;D ;F ;G ; 		// "S"
		case 86 : SEG1 = 0b00111110; break;  //B ;C ;D ;E ;F ; 		// "V"
		case 100: SEG1 = 0b01011110; break;  //B ;C ;D ;E ;G ; 		// "d"
		case 104: SEG1 = 0b01110100; break;  //C ;E ;F ;G ; 		// "h"
		case 200: SEG1 = 0b00000001; break;  //A ; 			// Seg A
		case 201: SEG1 = 0b00000010; break;  //B ; 			// Seg B
		case 202: SEG1 = 0b00000100; break;  //C ; 			// Seg C
		case 203: SEG1 = 0b00001000; break;  //D ; 			// Seg D
		case 204: SEG1 = 0b00010000; break;  //E ; 			// Seg E
		case 205: SEG1 = 0b00100000; break;  //F ; 			// Seg F
		default: break;																			
	}
	switch(Zeichen2){				
		case 1  : SEG2 = 0b01001001; break;  //A ;D ;G ; 		// Seg A+G+D
		case 2  : SEG2 = 0b00110110; break;  //B ;C ;E ;F ;		// Sonderzeichen
		case 3  : SEG2 = 0b00001001; break;  //A ;D ; 			// Seg A+D
		case 4  : SEG2 = 0b00011011; break;  //A ;B ;E ;D ; 		// Seg A+B+D+E
		case 5  : SEG2 = 0b00101101; break;  //F ;A ;C ;D ; 		// Seg F+A+C+D
		case 6  : SEG2 = 0b00110111; break;  //A ;B ;C ;E ;F ; 		// Seg A+B+C+E+F = spezielles "N"
		case 32 : SEG2 = 0b00000000; break;  //				// Blank
		case 45 : SEG2 = 0b01000000; break;  //G ; 			// "-"
		case 48 : SEG2 = 0b00111111; break;  //A ;B ;C ;D ;E ;F ; 	// "0"
		case 49 : SEG2 = 0b00000110; break;  //B ;C ; 			// "1"
		case 50 : SEG2 = 0b01011011; break;  //A ;B ;D ;E ;G ; 		// "2"	
		case 51 : SEG2 = 0b01001111; break;  //A ;B ;C ;D ;G ; 		// "3"
		case 52 : SEG2 = 0b01100110; break;  //B ;C ;F ;G ; 		// "4"
		case 53 : SEG2 = 0b01101101; break;  //A ;C ;D ;F ;G ; 		// "5"
		case 54 : SEG2 = 0b01111101; break;  //A ;C ;D ;E ;F ;G ; 	// "6"
		case 55 : SEG2 = 0b00000111; break;  //A ;B ;C ; 		// "7"
		case 56 : SEG2 = 0b01111111; break;  //A ;B ;C ;D ;E ;F ;G ; 	// "8"
		case 57 : SEG2 = 0b01101111; break;  //A ;B ;C ;D ;F ;G ;	// "9"
		case 61 : SEG2 = 0b01001000; break;  //D ;G ; 			// "="
		case 65 : SEG2 = 0b01110111; break;  //A ;B ;C ;E ;F ;G ;	// "A"
		case 69 : SEG2 = 0b01111001; break;  //A ;D ;E ;F ;G ; 		// "E"
		case 70 : SEG2 = 0b01110001; break;  //A ;E ;F ;G ; 		// "F"
		case 76 : SEG2 = 0b00111000; break;  //D ;E ;F ; 		// "L"
		case 83 : SEG2 = 0b01101101; break;  //A ;C ;D ;F ;G ; 		// "S"
		case 86 : SEG2 = 0b00111110; break;  //B ;C ;D ;E ;F ; 		// "V"
		case 100: SEG2 = 0b01011110; break;  //B ;C ;D ;E ;G ; 		// "d"
		case 104: SEG2 = 0b01110100; break;  //C ;E ;F ;G ; 		// "h"
		case 200: SEG2 = 0b00000001; break;  //A ; 			// Seg A
		case 201: SEG2 = 0b00000010; break;  //B ; 			// Seg B
		case 202: SEG2 = 0b00000100; break;  //C ; 			// Seg C
		case 203: SEG2 = 0b00001000; break;  //D ; 			// Seg D
		case 204: SEG2 = 0b00010000; break;  //E ; 			// Seg E
		case 205: SEG2 = 0b00100000; break;  //F ; 			// Seg F
		default: break;																			
	}

}	


In einer Interrupt-Routine wird das Backpannel-Signale des LCD fortlaufend umgeschaltet. Die Segment-Signale werden gleich- oder gegenphasig geschaltet, anhängig davon ob sie aktiv sein sollen oder nicht. Der gewünschte Zustand steht in den Variablen SEG1 und SEG2, jeweils 1 Bit pro Segment.

 
ISR(TIMER1_COMPA_vect)
{	// Output compare, LCD-Signal-Toggle, Dauer ~19us
	// Timer1, 16 Bit, Clock: 16MHz / 1024 / 64 = 244Hz -> ~4ms
	// - > Frequenz 122Hz (= 2 * toggle) 

	if (PhaseCount == 0) PhaseCount = 1;else PhaseCount = 0;	// wechselweise umschalten
	if (PhaseCount == 0) Backpanel = 1; else Backpanel = 0;

	// SegX auf 0 setzen wenn aktiv, sonst 1

	// Bit	PC	Wert
	//	0	0	1
	//	0	1	0
	//	1	0	0
	//	1	1	1
	//	Wert = !(Bit XOR PC)
	

	Seg1A  = !( (!(SEG1 & 0b00000001)) != (!PhaseCount) );
	Seg1B  = !( (!(SEG1 & 0b00000010)) != (!PhaseCount) );
	Seg1C  = !( (!(SEG1 & 0b00000100)) != (!PhaseCount) );
	Seg1D  = !( (!(SEG1 & 0b00001000)) != (!PhaseCount) );
	Seg1E  = !( (!(SEG1 & 0b00010000)) != (!PhaseCount) );
	Seg1F  = !( (!(SEG1 & 0b00100000)) != (!PhaseCount) );
	Seg1G  = !( (!(SEG1 & 0b01000000)) != (!PhaseCount) );
	Seg1DP = !( (!(SEG1 & 0b10000000)) != (!PhaseCount) );
	
	Seg2A  = !( (!(SEG2 & 0b00000001)) != (!PhaseCount) );
	Seg2B  = !( (!(SEG2 & 0b00000010)) != (!PhaseCount) );
	Seg2C  = !( (!(SEG2 & 0b00000100)) != (!PhaseCount) );
	Seg2D  = !( (!(SEG2 & 0b00001000)) != (!PhaseCount) );
	Seg2E  = !( (!(SEG2 & 0b00010000)) != (!PhaseCount) );
	Seg2F  = !( (!(SEG2 & 0b00100000)) != (!PhaseCount) );
	Seg2G  = !( (!(SEG2 & 0b01000000)) != (!PhaseCount) );
	Seg2DP = !( (!(SEG2 & 0b10000000)) != (!PhaseCount) );
}


Das Umpolen der Segmente muss immer mit genau 50% Dutycyle erfolgen damit sich im Mittel keine DC-Spannung über den Segmenten bildet. Andernfalls wird das LCD langsam durch Materialwanderung zerstört. Entsprechend muss die Interrupt-Routine immer die gleiche Laufzeit aufweisen und darf auch nicht durch andere Interrupts verzögert werden.

Wird fortgesetzt ...