|
|
High-Speed capture mit ATmega Timervon Michael Dreher Dieser Artikel nimmt am Artikelwettbewerb 2012/2013 teil. 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. [Bearbeiten] Unterschied zwischen Messung und ProtokollerkennungWenn 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):
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“. [Bearbeiten] HardwareaufbauDer Hardwareaufbau des IR-Analysators ist denkbar einfach:
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'.
[Bearbeiten] Software[Bearbeiten] Auflösung und Wertebereich der MessungenFü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:
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. [Bearbeiten] Vergleich Logic Analyzer Messung mit ATmega MessungNachfolgende 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:
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. [Bearbeiten] Warten auf Flankenwechsel mit ISRAls 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. [Bearbeiten] Warten auf Flankenwechsel mit PollingDie 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:
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
Wird vom avr-gcc übersetzt zu
In dieser Schleife passiert folgendes:
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. [Bearbeiten] ZeitmessungUm die Zeitdifferenz zwischen zwei Ereignissen zu messen gibt es mehrere Möglichkeiten:
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. [Bearbeiten] Keine Angst vor ÜberläufenEs gibt unterschiedliche Überläufe zu betrachten:
Die Differenz zwischen T(7) und T(6) muss über die Differenz von Timer1 und ovlCnt bestimmt werden:
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):
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:
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 '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. [Bearbeiten] Speicherung der ermittelten WerteDie 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
Wenn das Programm erweitert wird, oder in einer anderen Umgebung eingesetzt wird, muss die Konstante 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:
Der Verschiebungsfaktor zwischen den beiden Messbereichen kann über die Konstante RANGE_EXTENSION_BITS (Standardwert: 4 Bits) geändert werden. Es gilt:
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 [Bearbeiten] Ende der ErfassungFür den Abbruch der Erfassung gibt es zwei Kriterien:
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. [Bearbeiten] Ausgabe der DatenDie 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:
[Bearbeiten] Source CodeJetzt 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
[Bearbeiten] DownloadsDas 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.
[Bearbeiten] Siehe auch
[Bearbeiten] LizenzDieser Artikel unterliegt der Creative Commons Attribution-Share Alike Lizenz (CC BY-SA 2.0 DE) |