High-Speed capture mit ATmega Timer

Wechseln zu: Navigation, Suche

von Michael Dreher

Dieser Artikel nimmt am Artikelwettbewerb 2012/2013 teil.

Verarbeitung einer Messung in LibreOffice

Die Timer der ATmega Mikrocontrollerreihe bieten Unterstützung für vielfältige Aufgaben wie PWM, zyklische IRQ Aufrufe oder als Zeitbasis für Messungen. In diesem Artikel geht es um die Input Capture Funktionalität, welche sehr präzise Zeitmessungen ermöglicht, da Programmlaufzeiten und Verzögerungen für die Genauigkeit keine Rolle spielen. Bei einem Flankenwechsel eines externen Signals an einem ICPn Pin speichert der Timer seinen aktuellen Wert im ICRn Register.

Das hier beschriebene Projekt zeichnet die High- und Low-Zeiten eines externen Binärsignals auf. Nach der Aufzeichnung wird es analysiert und ausgegeben. Der Focus des Artikels liegt auf der Lösung der bei der Umsetzung auftretenden Probleme.

Der Begriff "Highspeed Capture" ist natürlich relativ zur Taktfrequenz des ATmega zu sehen. Das realisierte Programm speichert bis zu 3600 Flankenwechsel mit einer Auflösung von 62,5 ns. Die Zeitspanne zwischen zwei Flanken muss zwischen 3 µs und 65535 µs liegen.

Unterschied zwischen Messung und Protokollerkennung[Bearbeiten]

Wenn das Protokoll und damit der grobe Verlauf eines externen Signals bereits bekannt ist, kann man sich dessen Eigenschaften zu nutze machen und die Hard- und Software bestmöglich darauf abstimmen. Als Beispiel sei ein IR-Empfänger für Signale von IR-Fernbedienungen genannt.

Fernbedienungen modulieren ihr Ausgangssignal typischerweise mit Frequenzen zwischen 36 und 40 kHz. Es gibt Bauelemente (TSOP1736, 38, 40) um diese Modulation beim Empfang für eine bessere Signalempfindlichkeit auszunutzen (Fremdlichtunterdrückung) und das Signal zu demodulieren

Der Mikrocontroller bekommt vom TSOP17xx ein bereits aufbereitetes Signal, welches Pulsbreiten von mindestens 200 µs enthält (ein kompletter Burst aus z.B. 10 einzelnen Perioden), was gegenüber einer Einzel-Pulsbreite von 6 µs bei einem modulierten Signal sehr viel einfacher zu verarbeiten ist.

Hier ein Vergleich der Signale von SDP8600 (Channel 0) mit TSOP1738 (Channel 1, Ausgang negiert): HighSpeedCaptureAtmegaTimer comarison TSOP1738.png

Bei der Analyse und Vermessung von Signalen wird eine sehr viel schnellere und exaktere Erfassung benötigt als bei der Verarbeitung von bekannten Signalen.

Als Untersuchungsobjekt wurde ein IR-Signal gewählt, in der Praxis kann aber jedes beliebige Signal vermessen werden, sofern der Abstand zwischen zwei Flanken größer als ca. 2,8 µs ist. Bei der IR-Messung werden die Modulationsfrequenz und das Puls/Pause Verhältniss bestimmt und zusätzlich die Zeitwerte ausgegeben. Es geht explizit nicht darum ein bestimmtes IR Protokoll (wie z.B. RC5) zu implementieren.

Der Aufbau kann auch dazu dienen eine bestehende Implementierung eines IR-Senders zu überprüfen, quasi als einfacher Ein-Kanal „Logik Analyzer“.

Hardwareaufbau[Bearbeiten]

Aufbau mit Arduino Leonardo

Der Hardwareaufbau des IR-Analysators ist denkbar einfach:

  • Ein ATmega-Board. Getestete Arduino Boards siehe Tabelle unten.
  • Ein Honeywell SDP8600 Infrarot Optoschmitt Detektor (wichtig: im Gegensatz zu TSOP17xx ohne eingebauten Demodulator)
  • Ein Stützkondensator von 0,1 µF parallel zur Spannungsversorgung des SPD8600

Der SDP8600 wird an GND, +5V und Pin ICP1 des ATmega angeschlossen, da der 16-Bit Timer1 verwendet wird.

Beim Arduino Mega 2560 Board sind die ICP Pins ICP1 und ICP3 nicht nach außen geführt, daher muss auf ICP4 oder ICP5 ausgewichen werden.

Zum Debuggen habe ich einen Ausgangs-Pin des ATmega an einen Logic Analyzer angeschlossen. Er wird vom Programm gesetzt, wenn ein Flankenwechsel erkannt wird und zurückgesetzt, wenn das Abspeichern des Wertes beendet ist. Diesen Pin bezeichne ich im weiteren als 'Dbg Pin'.

Verwendete Pins und Arduino Digital Pin Nummern
MCU Arduino Board ICP Pin Dbg Pin Anzahl speicherbarer Werte Arduino Pin Mapping Tabelle
ATmega328P, ATmega168 Duemilanove, Uno ICP1 / PB0
Arduino Pin 8
PD6
Arduino Pin 6
1080 PinMapping168
ATmega32U4 Leonardo ICP1 / PD4
Arduino Pin 4
PD6
Arduino Pin 12
1500 PinMapping32u4
ATmega2560 Mega 2560 ICP4 / PL0
Arduino Pin 49
PL6
Arduino Pin 43
3600 PinMapping2560

Software[Bearbeiten]

Auflösung und Wertebereich der Messungen[Bearbeiten]

Für die Zeitmessung wird der 16-Bit Timer1 verwendet. Dieser hat eine Auflösung von bis zu 1/F_CPU. Beim Arduino entspricht dies 1/16 MHz = 62,5 ns. Für den maximalen Timer-Wert 65535 ergibt dies eine Zeitspanne von 4,096 ms. Längere Zeiträume können bei dieser Auflösung nicht direkt mit dem Hardware-Timer gemessen werden. Für einige Anwendungsfälle ist dieser Zeitraum zu kurz.

Es gibt mehrere Möglichkeiten den Zeitraum zu erweitern:

  • Timer Prescaler (Vorteiler) für das Taktsignal des Timers verwenden. Über die Prescaler Bits CS12 bis CS10 in Register TCCR1B kann man das System-Taksignal der CPU um folgende Faktoren runterteilen: 1, 8, 64, 256, 1024
  • Software-Überlaufbehandlung des Timers

Der Nachteil beim Einsatz des Prescalers ist, dass sich die Auflösung dadurch verringert. Um Zeiträume von bis zu 65 ms erfassen zu können, müsste ein Prescaler-Wert von 64 verwendet werden. Dadurch würde die Auflösung von 62,5 ns auf 4 µs sinken. Damit könnte der genannte Zweck, Zeiten in der Größenordnung von 8 µs mit einer Genauigkeit von +-2% genau zu vermessen um die Modulationsfrequenz zu bestimmen, nicht erreicht werden. Aus diesem Grund verwendet dieses Projekt die Überlaufbehandlung.

Die Überlaufbehandlung inkrementiert einen Software-Zähler, wenn der Wertebereich des Timers überschritten wird. Damit können beide Ziele erreicht werden: eine hochauflösende Messung mit einem großen Messbereich.

Vergleich Logic Analyzer Messung mit ATmega Messung[Bearbeiten]

Nachfolgende Grafik zeigt die Erfassung zweier Pulse, mit einer ON-Zeit von 4,5 µs und einer OFF-Zeit von 21,6 µs. Die Messung wurde parallel mit dem ATmega und einem Logic-Analyzer durchgeführt und die Kurven übereinandergelegt. Die orangene Linie 'LA' zeigt die Logic-Analyzer Messung, die schwarze Linie 'AVR' die ATmega Messung. Die Zeitskala ist in Sekunden, die Grafik zeigt einen Zeitraum von 40 µs. Wie man sieht, weichen die beiden Kurven kaum voneinander ab:

HighSpeedCaptureAtmegaTimer 3Pulse.png

Die blaue 'Dbg' Kurve zeigt die Rückmeldung vom ATmega Programm an den Logic Analyzer. Hier kann man die Reaktionszeit (0,79 µs) und die Programmlaufzeit für die Bearbeitung der Flanken (2,38 µs) ablesen. Durch den aktivierten noise canceler (Bit ICNC1 in TCCR1B) ist diese Kurve gegenüber den anderen beiden um 4 CPU Zyklen (0,25 µs) nach rechts verschoben.

Warten auf Flankenwechsel mit ISR[Bearbeiten]

Als erster Ansatz wurde eine ISR (Interrupt Service Routine) verwendet, welche bei jedem Flankenwechsel getriggert wurde. Dies war zu langsam um mit dem Signal schritthalten zu können und wurde daher verworfen.

Der System Overhead beim Aufruf einer ISR in C ist recht hoch, da Register gesichert und wieder restauriert werden müssen (5 Register, insgesamt ca. 13 Zyklen, siehe diesen Artikel). Hinzu kommen noch 4 Zyklen, bis zur Ausführung der ersten Instruction der ISR, außerdem können andere IRQs (z.B. der Arduino Timer0 IRQ oder USB IRQs vom ATmega32U4) ins Gehege kommen und die Erfassung ausbremsen.

Für die Speicherung von Zuständen müssen globale Variablen verwendet werden, welche dann nicht in Registern gehalten werden können und jedes mal neu geladen werden. Man kann zwar auch globale Variablen in Register legen, diese stehen dann aber im restlichen Programm nicht mehr zur freien Verfügung.

Für die Variablen auf die von der ISR und vom normalen Programm zugegriffen wird, muss der type qualifier “volatile“ verwendet werden, was dem Compiler einiger Optimierungsmöglichkeiten beraubt und den Code unter Umständen nochmal deutlich langsamer macht.

Warten auf Flankenwechsel mit Polling[Bearbeiten]

Die schnellere Variante der Flankenabfrage ist polling des Timer Flag Registers. Der grobe Ablauf des Programms ist sehr einfach. Hier der Pseude-Code für die komplette Erfassungs-Schleife:

IRQs deaktivieren
While(Ende nicht erreicht)
{
	Warten auf einen Flankenwechsel (Bit ICF1 in TIFR1)
	Auslesen des Input Capture Wertes (ICR1)
	Speichern der Zeitdifferenz zum letzten Ereignis in einem Array
	Trigger für Timer invertieren (TCCR1B ^= _BV(ICES1))
}
IRQs erlauben

Die Schleife für das Warten auf den Flankenwechsel besteht aus nur 3 Assembler Instructions, was deutlich kürzer und schneller ist als die ISR Variante.

Der C-Code

uint8_t tifr;
while(! (tifr = (TIFR1 & (_BV(ICF1) | _BV(OCF1A))))) {
}

Wird vom avr-gcc übersetzt zu

 
loop:
in      r18, TIFR1
andi    r18, _BV(ICF1) | _BV(OCF1A)
breq    loop

In dieser Schleife passiert folgendes:

  1. Es wird abgefragt, ob ein Ereignis aufgetreten ist, für welches ein Timer-Wert durch den Input Capture gespeichert wurde (TIFR1 & (_BV(ICF1))
  2. Es wird abgefragt, ob der Output Compare erkannt hat, dass ein Differenz-Überlauf passiert ist (TIFR1 & (_BV(OCF1A)), Details siehe Abschnitt Keine Angst vor Überläufen
  3. Der Wert der UND Verknüpfung wird gespeichert um ihn weiter unten weiterzuverwenden und TIFR nicht erneut auslesen zu müssen.

Der im Arduino Leonardo verbaute ATmega32U4 verwendet keinen externen USB-seriell Wandler sondern implementiert direkt das USB Protokoll. Reagiert ein USB Device einige Zeit nicht auf die Anfragen des USB Hosts, wird es abgekoppelt. Dies passiert z.B. wenn man die IRQ Bearbeitung für einige Zeit abschaltet wie es in diesem Projekt gemacht wird. Ein Workaround ist, die IRQs nur so kurz wie möglich zu sperren, d.h. erst nachdem die erste Flanke erkannt wurde. Eine stabile Lösung ist dies aber nicht, daher ist für diese Anwendung der ATmega32U4 nicht zu empfehlen.

Zeitmessung[Bearbeiten]

Um die Zeitdifferenz zwischen zwei Ereignissen zu messen gibt es mehrere Möglichkeiten:

  1. Nach jedem Ereignis den Timer auf 0 zurücksetzen
  2. Den Timer weiterlaufen lassen und immer die Differenz zum letzten Timer-Wert zu bilden.

Die erste Variante hört sich erst einmal verlockend einfach an. Zwischen dem Erkennen einer Flanke und dem Zurücksetzen des Timers vergehen aber einige µs. Der nächste Timer Messwert wäre genau um diesen Versatz zu klein. Sofern dieser Versatz bekannt ist, kann man ihn herausrechnen, bei einem Test lag er bei mir bei 30 CPU Zyklen. Mit jeder Programm- oder Compileränderungen muss dieser Versatz aber neu bestimmt werden.

Die zweite Variante ist eleganter, da sie den Timer durchlaufen lässt und die Differenz zwischen zwei Timer-Werten bildet. Ein Versatz wird dadurch komplett vermieden.

Keine Angst vor Überläufen[Bearbeiten]

Es gibt unterschiedliche Überläufe zu betrachten:

  1. Überlauf des Timer Wertes TCNT1. Dies ist kein Problem und wird durch die vorzeichenlose Modulo 2^16 Berechnung der Differenz kompensiert, Rechenbeispiel siehe unten
  2. Überlauf der Differenz zwischen zwei Zeitpunkten, d.h. wenn die Differenz zwischen zwei Flanken größer als 65535 ist (Zeitspanne größer als 4,096 ms). Diese Überlaufe müssen mitgezählt werden (in ovlCnt) und als obere Bits der Zeitdifferenz gespeichert werden.
Beispiele für erfasste Timer-Werte
Zeitpunkt Signal CLKs Timer1 T(n) – T(n-1) ovlCnt Timer1 (in HEX)
T(5) 0->1 10000 10000 0x2710
T(6) 1->0 43600 43600 33600 0 0xAA50
T(7) 0->1 206444 9836 162844 2 0x266C

Die Differenz zwischen T(7) und T(6) muss über die Differenz von Timer1 und ovlCnt bestimmt werden:

T(7) - T(6) = (9836 - 43600) MOD 2^16 + 2^16 * 2 (ovlCnt) = 31772 + 2^16 * 2 = 162844

Die Differenz-Berechnung erfolgt Modulo 2^16, da der Datentyp uint16_t verwendet wird. Diese Art der Berechnung ist etwas gewöhnungsbedürftig, wenn der Subtrahend größer ist als der Minuend und daher ein negatives Ergebnis erwartet wird. Daher hier eine genauere Ausführung in hexadezimaler Schreibweise (damit man die 16-Bit Wortgrenze erkennen kann):

T(7) - T(6) = 0x266C – 0xAA50 = 0xFFFF7C1C (als uint32_t)
T(7) - T(6) = 0xFFFF7C1C MOD 2^16 = 0x7C1C (als uint16_t) = 31772

Um Differenzwerte größer als 65535 zu verarbeiten, muss man die Überläufe der Timer-Differenz mitzählen, was in der Variable ovlCnt passiert. Zwischen den Zeitpunkten T(7) und T(6) hat Timer1 den Wert 43600 (Timer-Wert bei T(6)) noch zweimal erreicht, d.h. die Differenz ist 2 mal übergelaufen und der Wert um 2*2^16 größer:

T(7) - T(6) = (0x266C – 0xAA50) + 2*2^16 = 0x7C1C + 2 * 0x10000 = 027C1Cx = 162844

Mehr zur Theorie der Modulo-2^x Arithmetik (Restklassenring) ist unter Siehe auch zu finden.

Hier eine Grafik, welche den Verlauf von Timer1, die Erfassungs-Zeitpunkte (Capture) und das Hochzählen von ovlCnt darstellt
HighSpeedCaptureAtmegaTimer Overflows.png

'Capture' bezeichnet die Erfassungszeitpunkte T(5) bis T(7), bei welchen Timer1 aufgrund der Flanke von 'Signal' den aktuellen Wert speichert.

Damit man nicht „von Hand“ vergleichen muß, ob der Wert 43600 zwischen zwei Flanken nochmal vorkam (die Zeitpunkte mit 'ovlCnt++' in der Grafik markiert sind), kann man den Output Compare Wert des Timers setzen und dies die Hardware des ATmega machen lassen. Wenn der Timer diesen Wert erreicht, wird das Output Compare Flag OCF1A im Timer Flag Register TIFR1 gesetzt und daraufhin die Variable ovlCnt erhöht. Nach jeder Erfassung eines Timer-Wertes wird OCF1A auf den erfassten Timer-Wert gesetzt.

Speicherung der ermittelten Werte[Bearbeiten]

Die Zeit-Differenzwerte werden in einem Array gespeichert. Das für die Wertspeicherung verfügbare RAM hängt davon ab, wie viel Speicher für andere Zwecke benötigt wird (Stack, globale Variablen). Bei Arduino 1.0.3 mit ATmega328P (2048 Byte RAM) bleibt Platz für ca. 870 16-Bit Werte, mit dem ATmega32U4 kommt man auf ca. 1050 und mit dem ATmega2560 kommt man auf 3600 Werte. Die Puffergröße im Programm ist relativ zur Arbeitsspeichergröße RAMEND festgelegt, d.h. bei größerem Speicher erweitert sich automatisch der Puffer. Der vom Programm belegte Speicher wird durch projectRamUsage festgelegt:

const uint16_t projectRamUsage = 380 + 64; // das absolute Minimum ist 240 + 64
const uint16_t bufSize ((RAMEND - 0x100 - projectRamUsage) / sizeof(uint16_t));

Wenn das Programm erweitert wird, oder in einer anderen Umgebung eingesetzt wird, muss die Konstante projectRamUsage angepasst werden, da es sonst abstürzt.

Bei einer Auflösung von 62,5 ns können in einem uint16_t Wert maximal Zeiten von 4 ms gespeichert werden. Um den Messbereich zu erweitern greift man zu einem Trick. Bei kurzen Zeitspannen ist die absolute zeitliche Auflösung sehr wichtig, damit die prozentuale Ungenauigkeit nicht zu groß wird. Um bei langen Zeitspannen dieselbe prozentuale Genauigkeit zu erhalten, benötigt man eine geringere absolute zeitliche Auflösung. Dies ist vergleichbar mit der Messbereichsumschaltung bei einem Digitalmultimeter.

Dies könnte man dadurch erreichen, dass man Fließkommazahlen verwendet, welche durch die getrennte Verarbeitung des Exponenten eine automatische Messbereichsumschaltung eingebaut haben. Diese haben aber den Nachteil, dass die Verarbeitung ohne Hardwareunterstützung auf einem Mikrocontroller sehr langsam ist und sie viel Platz benötigten (4 Byte für einfache oder sogar 8 Byte für doppelte Genauigkeit pro Wert).

Eine Alternative ist eine spezielle Codierung, des 16-Bit Wertes. Das höchste Bit wird als Umschalter zwischen zwei Messbereichen verwendet:

  • Messbereich 1: Wenn Bit 2^15=0 ist, dann enthält der Wert die Differenz-Zeit ohne weitere Skalierung. Damit sind alle Werte zwischen 0 und 32767 ohne Lücken direkt darstellbar (zeitliche Auflösung 62,5 ns, maximal 2,048 ms).
  • Messbereich 2: Wenn Bit 2^15 den Wert 1 hat, enthält die Zahl die Differenz-Zeit mit einer zeitlichen Auflösung von 2 µs. Der Wertebereich und die zeitliche Auflösung sind also jeweils um den Faktor 32 verschoben. Dadurch sind Zeiten bis zu 65,535 ms darstellbar.

Der Verschiebungsfaktor zwischen den beiden Messbereichen kann über die Konstante RANGE_EXTENSION_BITS (Standardwert: 4 Bits) geändert werden. Es gilt:

Verschiebungsfaktor = 2^(RANGE_EXTENSION_BITS + 1)

Um die Zahlen schneller verarbeiten zu können und weniger Schiebebefehle bei der Ablage zu benötigen werden im Messbereich 2 die Bits 2^16 bis 2^19 (der Überlauf-Zähler) in den unteren 4 Bits abgelegt und erst beim Auslesen in die korrekte Position geschoben, siehe Funktion packTimeVal(). Beim Auslesen der Daten ist dann eine Dekodierung notwendig. Dies erfolgt außerhalb der zeitkritischen Erfassungs-Schleife in der Funktion unpackTimeVal().

Ende der Erfassung[Bearbeiten]

Für den Abbruch der Erfassung gibt es zwei Kriterien:

  1. Der Puffer für die Speicherung der Werte ist voll
  2. Die maximal darstellbare Zeit wurde überschritten (entspricht einem Timeout, d.h. kein Ereignis für eine bestimmte Zeit). Dies passiert, wenn 65 ms lang keine Flanke erkannt wird.

Am Anfang der Erfassung wird endlos auf eine low->high Flanke gewartet, da man sonst nach dem Start der Erfassung innerhalb von 65 ms das zu analysierende Signal anlegen müsste.

Ausgabe der Daten[Bearbeiten]

Die erfassten Werte werden in drei Formaten über die serielle Schnittstelle ausgegeben.

1. In einem kompakten Format, bei welchem die einzelnen High- und Low- Zeiten durch + und - markiert sind, die Werte sind in ns

capture[1056 values]=
+9625 -16625 +9625 -16625 +9687 -16562 +9687 -16625 +9625 -16625 +9625 -16625 \
+9687 -16625 +9625 -16625 +9625 -16625 +9687 -799562
+9625 -16625 +9625 -16625 +9687 -16562 +9687 -16625 +9625 -16625 +9625 -16625 \
+9687 -16625 +9625 -16625 +9625 -16625 +9687 -1855375
+9625 -16625 +9687 -16562 +9687 -16625 +9625 -16625 +9625 -16625 +9687 -16625 \
+9625 -16625 +9625 -16625 +9625 -16625 +9687 -799687
+9625 -16625 +9687 -16562 +9687 -16625 +9625 -16625 +9625 -16625 +9687 -16562 \
+9687 -16625 +9625 -16625 +9625 -16625 +9687 -799687
...


2.
Verarbeitung einer Messung in LibreOffice
Im CSV-Format welches direkt in OpenOffice oder Excel importiert werden kann um ein Diagramm zu erstellen. Für jede Flanke werden zwei Zeilen ausgegeben um eine senkrechte Linie im Diagramm darzustellen:
"Time [ns]";"Signal";"Duration [ns]"
0;0;0
1;1;9625
9625;1;0
9626;0;16625
26250;0;0
26251;1;9625
35875;1;0
35876;0;16625
52500;0;0
52501;1;9687
...


3. Als Zusammenfassung werden die Periodendauer und die Modulationsfrequenz ausgegeben (hierbei werden nur Pulse/Perioden berücksichtigt, die maximal 10% über der kürzesten Periodendauer liegen) :
Number of sample periods used for average values: 720
Carrier frequency [Hz]: 38186
Medium period [ns]: 26187
Medium pulse width [ns]: 10875
Duty cycle [%]: 41

Source Code[Bearbeiten]

Jetzt geht es endlich ans Eingemachte! Der hier abgedruckte Source Code ist ein vereinfachter Ausschnitt mit den Funktionen auf die im Text eingegangen wurde. Es fehlen die Anpassungen für ATmega32U4 und ATmega2560. Das gesamte Projekt mit dem Code für die andere MCUs ist im Abschnitt Downloads zu finden

// Copyright (c) 2012 Michael Dreher  <michael(at)5dot1.de>
// this code may be distributed under the terms of the General Public License V2 (GPL V2)
 
uint16_t captureData[bufSize]; // the buffer where the catured data is stored
int16_t captureCount; // number of values stored in captureData
 
// Wait for a signal on pin ICP1 and store the captured time values in the array 'captureData'
void startCapture(void)
{
  unsigned char sreg = SREG;
  cli(); // disable IRQs
  register uint8_t ices1_val = _BV(ICES1); 
  register uint8_t tccr1b = TCCR1B | ices1_val; // trigger on rising edge
  TCCR1B = tccr1b;
  OCR1A = TCNT1 - 1;
  TIFR1 = _BV(ICF1) | _BV(OCF1A) | _BV(TOV1); // clear all timer flags
  register uint16_t ovlCnt = 0;
  register uint16_t prevVal = 0;
  register uint8_t tifr; // cache the result of reading TIFR1 (masked with ICF1 and OCF1A)
  register uint16_t *pCapDat; // pointer to current item in captureData[]
 
  for(pCapDat = captureData; pCapDat <= &captureData[bufSize - 1]; )
  {
    // wait for edge or overflow (output compare match)
    while(! (tifr = (TIFR1 & (_BV(ICF1) | _BV(OCF1A))))) {
    }
    uint16_t val = ICR1;
    OCR1A = val; // timeout based on previous trigger time
 
    if(tifr & _BV(OCF1A)) // check for overflow bit
    {
      if(pCapDat != captureData) // ignore overflow at the beginning of the capture
      {
        ovlCnt++;
        if(ovlCnt >= _BV(RANGE_EXTENSION_BITS))
        {
          TIFR1 = _BV(ICF1) | _BV(OCF1A); // clear input capture and output compare flag bit
          break; // maximum value reached, treat this as timeout and abort capture
        }
      }
      TIFR1 = _BV(ICF1) | _BV(OCF1A); // clear input capture and output compare flag bit
      continue;
    }
 
    tccr1b ^= ices1_val; // toggle the trigger edge
    TCCR1B = tccr1b;
    TIFR1 = _BV(ICF1) | _BV(OCF1A); // clear input capture and output compare flag bit
 
    uint16_t diffVal = val - prevVal;
    if(ovlCnt || (diffVal & 0x8000))
    {
      diffVal = packTimeVal(diffVal, ovlCnt);
      ovlCnt = 0;
    }
 
    *pCapDat = diffVal;
    pCapDat++;
    prevVal = val;
  }
 
  // the first array entry contains only the starting time and no time
  // difference, therefore ist is no longer needed and will be removed
  captureCount = (pCapDat - captureData) - 1; // correct count
  for(int16_t i = 0; i < captureCount; i++)
  {
    captureData[i] = captureData[i + 1];
  }
  SREG = sreg; // enable IRQs
}
 
inline uint16_t packTimeVal(uint16_t diffVal, uint16_t ovlCnt)
{
  // overflow part is stored in the lower RANGE_EXTENSION_BITS bits and not in
  // the upper bits because that makes the code smaller here (less shifting)
  return (0x8000 | ((diffVal >> 1) & (0x7fff & ~(_BV(RANGE_EXTENSION_BITS) - 1))) | ovlCnt);
}  
 
inline uint32_t unpackTimeVal(uint32_t val)
{
  if(val & 0x8000)
  {
    val = val & 0x7fff;
    uint32_t valOvl =  (val & (_BV(RANGE_EXTENSION_BITS) - 1)) << 16;
    uint32_t valTim =  (val << 1) & (0x7fff & ~(_BV(RANGE_EXTENSION_BITS) - 1));
    val = valOvl | valTim;
  }
 
  return val;
}

Downloads[Bearbeiten]

Das Download-Archiv enthält ein Arduino Projekt. Die Kern-Funktionalität (die Erfassung) kommt ohne die Arduino Libraries aus, die Arduino Funktionen werden nur die Ausgabe der Werte verwendet.

Enthält den SourceCode für ATmega168, ATmega328 und ATmega32U4. Diese Variante ist zu empfehlen, wenn man den Code lesen und verstehen will, da keine Makros für die Registernamen verwendet werden
Enhält den SourceCode für den ATmega2560. Alle Timer-Registernamen wurden durch Makros ersetzt, so dass man den verwendeten Timer durch die Änderung des zentralen defines CAP_TIM geändert werden kann, worunter leider die Lesbarkeit gelitten hat. Funktioniert natürlich weiterhin mit ATmega168, ATmega328 und ATmega32U4

Siehe auch[Bearbeiten]

Lizenz[Bearbeiten]

Dieser Artikel unterliegt der Creative Commons Attribution-Share Alike Lizenz (CC BY-SA 2.0 DE)