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.
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.
Das Display ist ein sog. statisches LCD DE112
mit 2 Dezimalstellen.
Der Arduino ist auf der Unterseite einer Lochpunktrasterplatte befestigt. Auf der Oberseite sitzt das Display und die elektronischen Bauteile.
Schaltplan
Im Schaltplan sind die zusätzlichen elektronischen Bauteile zu erkennen.
Die Verdrahtung am Motorad ist sehr simpel.
- 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.
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.
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).
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 ...