Christians Webseite        << zurueck        vor >>

Eine Ganganzeige selbst bauen, Teil 2, Software

Dieser Teil der DIY-Serie hat den Fokus auf Software. Alle Beispiele basieren auf einen Atmel ATmega328 in klassischer C-Toolchain-Umgebung. Mit kleinen Anpassungen bei den Registernamen ist alles auch auf den anderen ATmega-Geschwistern (z.B. ATmega128) lauffähig.

Das Prinzip der Frequenzmessung

Zur Frequenzbestimmung wird die Zeit zwischen 2 ansteigenden Flanken des Eingangssignals gemessen.
GA-DIY

Das Eingangssignal muss dafür auf einen Interrupt-fähigen Eingang gelegt werden (INT0 oder INT1). Die zugehörigen Interrupt-Register werden für das Erkennen einer ansteigenden Flanke gesetzt. In der Interrupt-Routine wird der Zählerstand eines freilaufenden Oszillators gespeichert. Der Zählerstand ist die Anzahl der abgelaufenen Taktimpulsen. Der Kehrwert entspricht der gesuchten Signalfrequenz.

Vorbereitung der Register

Counter/Timer0 (8-Bit) wird als freilaufender Zähler mit Overflow-Interrupt konfiguriert. Die Clock-Rate beträgt 125kHz. Der Overflow-Counter ist eine künstliche Vergrösserung des Zählbereichs. Der 8-Bit Zahlenbereich wäre für den gewählten Takt und die zu erwartenden Frequenzen etwas zu klein.

 
void timer0_init(void)
{   // 8-Bit Normal operation, Zähler für RPM-Intervall, Overflow-Interrupt aktiv
    // Normal operation, clock internal prescaler 011 (= Faktor 64)
    // Oszillatorfrequenz 8MHz, prescaler 64 -> Counter-Clock = 125kHz
    // 8Bit-Counter Overflow nach 2ms (488 Hz)

    TCCR0B  = 0b00000011;  		
    TCNT0   = 0;            // Zählerstand auf 0 setzen
    TIMSK0 |= (1 << TOIE0); // Overflow-Interrupt0 enabled
}

Die Befehle in der Overflow-Interrupt-Routine sind trivial, es wird nur eine Variable inkrementiert. Der Overflow-Counter ist als uint16_t deklariert. In der Gesamtsumme ergibt sich damit ein maximaler Zählbereich von 24 Bit (= 134 Sekunden). Das ist für alle praktischen Fälle ausreichend.
 
uint16_t Timer0_overflow;         // Überlaufvariable, global deklariert

ISR(TIMER0_OVF_vect)
{  // Overflow 8 Bit Timer

   if (Timer0_overflow < 0xFFFE) Timer0_overflow++;
}

Der externe Interrupt wird konfiguriert, auf Rising-Edge gesetzt und aktiviert.
 
 void Interrupt0_init(void)
{   // External Interrupt0 aktivieren, RPM

   EICRA |= (1 << ISC00)|(1 << ISC01);// Rising edge generates interrupt
   EIMSK |= (1 << INT0);              // Interrupt 0 set to active
}

Die wichtige Interrupt0-Routine. Sie wird bei einem Interrupt angesprungen. Hier wird der aktuelle Zählerstand von Counter0 ausgelesen. Hinzu werden die aufgetretenen Überläufe multipliziert. Das Endergebnis findet sich in der Variable "Ergebnis_RPM". Das Counter0-Zählregister wird nach dem Lesen zurückgesetzt.

uint32_t Ergebnis_RPM;         // Messwert des Drehzahlsignals, global deklariert

ISR(INT0_vect)
{  // Interrupt Eingang 0, RPM, 8-Bit Counter0 Werte übernehmen
   uint32_t dummy2;            // Hilfsvariable

   Ergebnis_RPM = TCNT0;       // untere 8 Bit aus Counter lesen
   TCNT0 = 0;                  // Counter zurücksetzen
   dummy2 = Timer0_overflow;   // 16 Bit vom Überlauf lesen
   Timer0_overflow=0;          // Überlauf zurücksetzen
   dummy2 = (dummy2 << 8);     // Überlauf um 8 Bit schieben, Platz machen für TCNT0	
   Ergebnis_RPM += dummy2;     // Ergebnis Zusammenbauen
}


Alle vorhergehenden Programmblöcke müssen analog auch noch für die Geschwindigkeitsmessung erstellt werden. Diesmal mit/für:

  • Counter/Timer2 (8-Bit)
  • uint16_t Timer2_overflow
  • INT1
  • uint32_t Ergebnis_Speed


Die Interrupt-Routinen sind zeitkritisch. Sie müssen beendet sein bevor ein neuer Interrupt auftreten kann. Für die Drehzahl wurde eine Frequenz von bis zu 4800 Hz angenommen (siehe Teil 1). Das entspricht 208 us oder 1666 Prozessortakten. Das klingt erstmal sehr bequem. Allerdings ist das Beispiel nicht vollständig, es fehlt noch etwas "Beiwerk". Eventuell müssen noch Mittelwerte berechnet und diverse Bereichsüberprüfungen durchgeführt werden.

Noch entscheidender ist die Response-Zeit für den zweiten (asynchron dazu) laufende Interrupt! Je länger die Interruptbearbeitung dauert desto wahrscheinlicher ist ein verzögerter Start in der jeweils anderen. Und damit ein falsch ausgelesener Zählerstand. Zum Glück ist die Statistik an dieser Stelle nicht kritisch und eine Überlappung relativ selten.

Die Gangberechung

Sobald die Interrupt-Routinen ihre Frequenz-Ergebnisse geliefert haben kann die eigentliche Gangberechung beginnen. Genaugenommen wurden die Periodendauern gemessen, also die Kehrwerte der Frequenzen. Aber das spielt keine Rolle weil nur das Verhältnis der beiden Werte zueinander wichtig ist. Es wird daher zuerst der Quotient aus beiden Messwerten bestimmt:

Quotient = (float)Ergebnis_RPM / (float)Ergebnis_Speed.

Dieser Quotientenwert ist für jeden Gang einmalig und konstant und unabhängig von der Drehzahl weil er ein direktes Abbild der mechanischen Übersetzung ist. Als Beispiel die Werte für eine Yamaha FZS1000:

  • Gang 1: 0,367
  • Gang 2: 0,498
  • Gang 3: 0,611
  • Gang 4: 0,688
  • Gang 5: 0,764
  • Gang 6: 0,822

Die Messergebnisse wurden bewusst vorab in einen "Float" umgewandelt. Eine Integer-Division würde wegen Rundungen zu hohe Abweichungen hervorrufen.

Der berechnete Quotient wird aus diversen Gründen nicht exakt mit dem Tabellenwert übereinstimmen. Es muss daher noch ein Vergleich auf minimale Abweichung folgen. Der Tabellenplatz mit der kleinsten Abweichung ist der gesuchte Gang :)


Q_MinPosition = 0;                        // Array-Position mit minimaler Differenz zum Messwert, default
Q_MinValue = 65000;                       // zugehöriger Differenzwert, default
lauf = 1;                                 // Laufindex

while(lauf < 7) {                                   // alle Quotienten der Tabelle durchlaufen
    Q_Delta = (abs(Q_Gangtabelle[lauf]-Q_Messung)); // Differenzen zw Messung und Ziel-Werten berechnen
    if (Q_Delta < Q_MinValue) {                     // Differenz kleiner als alter Wert?
        Q_MinValue = Q_Delta;                       // kleinste Abweichung merken
        Q_MinPosition = lauf;                       // Array-Index der kleinsten Abweichung merken
    }	
    lauf++;
}

Der gesuchte Gang ist der finale Wert in Q_MinPosition. Die Entscheidungsschwelle liegt jeweils in der Mitte zwischen zwei Q-Werten. Beispiel Gang 1 und Gang 2:
Q_[1] = 0,367
Q_[2] = 0,498
Mittelwert = 0,4325 = 0,367 + (0,498-0,367)/2

Um grobe Messfehler zu erkennen und um die Auswertung robuster zu machen kann man weitere Grenzwerte (bzw "Pseudo"-Gangwerte) unterhalb des 1ten und oberhalb des 6ten Gangs hinzufügen. Die Werte sind geschätzt und extrapoliert aus den nächstliegenden Abständen. Im Quotienten-Array werden sie einfach angehängt. Der Algorithmus findet sie und setzt ein Fehler-Flag.
Q_[7] = 0,236 = 0,367 - (0,498 - 0,367); Fehler unterhalb 1ten Gang
Q_[8] = 0,880 = 0,822 + (0,822 - 0,764); Fehler oberhalb 6ter Gang

Sonstige Berechnungen und Feinheiten

Eine wichtige Neben-Funktion ist es zu erkennen ob die Interrupt-Impulse ausbleiben. Dafür kann der noch ungenutzte Counter/Timer1 eingesetzt werden. Er überprüft regelmässig ob die Overflow-Zähler im normalen Bereich liegen. Sobald die Zähler unplausibel hohe Werte enthalten wird ein Fehlerflag gesetzt.


void timer1_init(void)
{   // 16-Bit, OutputCompareRegister aktiv, testet auf ausbleibende Interrupt der Zähleingänge 
    TCCR1A =	0b00000000;   // CTC Mode 4 clear on OCR1A
    TCCR1B =	0b00001011;   // CTC Mode 4 clear on OCR1A, clock internal prescaler
                              // Main-Clock = 8MHz; timer1-Clock = 125KHz (= 8MHz / 64)
    OCR1AH = 0x30;
    OCR1AL = 0x00;            // OCR-Interrupt = 125kHz/12288 = 10Hz -> 0,1 Sekunden Raster
    TCNT1H = 0;
    TCNT1L = 0;
    TIMSK1 |= (1 << OCIE1A);  // Output Compare A Match Interrupt enabled
}

Das Fehler-Flag wird gesetzt sobald einer der Overflow-Counter einen Wert von 1000 überschreitet. Das bedeutet es wurde während der letzten 2 Sekunden kein Interrupt ausgelösst. Das entsprechende Signal wird damit als nicht vorhanden angenommen und es kann kein Gang-Wert berechnet werden.

ISR(TIMER1_COMPA_vect)
{   // Output compare, Fehlerflag setzen wenn keine Speed oder RPM-Signal vorhanden sind
    SignalFailure = ((Timer0_overflow > 1000)||(Timer2_overflow > 1000));
}

Die Messergebnisse aus den Frequenzmessungen ändern sich permanent und sind typischerweise mit Störungen behaftet. Um die Gangerkennung stabiler und robuster zu machen ist es daher angebracht mit gleitenden Mittelwerten zu arbeiten. Einige Variablen sollten daher als Array deklariert werden und der Reihe nach (im Kreis) befüllt werden. Zu extensive Mittelungen sind aber nicht hilfreich weil das dynamische Verhalten stark verschlechtert wird..

uint32_t Ergebnis_RPM[4];   // Messergebnisse des Drehzahlsignals, global deklariert
uint32_t Ergebnis_Speed[4]; // Messergebnisse des Geschwindigkeitssignals, global deklariert
float Quotient[8];          // Ergebnisse Quotientenberechung


Main

In der Hauptschleife des Programm passiert die meiste Zeit nichts. Es wird auf neue Ergebnisse von den beiden Interrupts (INT0 und INT1) gewartet. Um das zu erkennen setzt man sich am besten ein paar Flags in die Interrupts.

Sobald zwei frische Ergebnisse vorhanden sind kann die Gangberechnung gestartet werden. Im Anschluss muss der neue Gang "nur noch angezeigt werden".

Das Grundgerüst der Software ist damit erstellt. Es fehlen allerdings noch jede Menge Feinheiten um die Messungen robuster zu machen. Und es wird nochmals deutlich umfangreicher wenn die Software universeller und für beliebige Motorräder einsetzbar werden soll.