Impulsgenerator für 7-Kanal-RC-Fernsteuerung

Warum eigentlich?

Eigentlich betreibe ich keinen Modellbau. Daher bin ich auch nicht im Besitz einer Fernsteueranlage. Nun wurde ich aber gebeten, einige AVR-Programme für Fahrtregler, Fernschalter usw. zu schreiben. Um diese testen und debuggen zu können, benötigte ich aber eine Fernsteueranlage, zumindest ein Gerät, das mir die Kanalimpulse (wie von einem Fernsteuerempfänger) zur Verfügung stellt.
Ziel war also ein Impulsteil eines Fernsteuersenders, dass ein HF-Teil (falls vorhanden) tasten (ansteuern) kann, aber auch Servos, Fahrtregler, Schaltdecoder usw. direkt ansteuern kann um diese zu testen. Dazu wurde erstmal dieses Gerät mit AT90S4433 gebaut, welches sich gut bewährt hat.
Inzwischen ist aber der 4433 nicht mehr erhältlich. Auch ist das damalige Programm aus heutiger Sicht sehr anfängerhaft programmiert. Und dann habe ich inzwischen diese kleinen Mini-Kreuzknüppel (siehe Bild) beschafft, die die Bedienung vereinfachen. Es kommt nun ein ATmega48 zum Einsatz, der mit dem internen 8MHz-Takt und Vorteiler 8 betrieben wird, also mit 1MHz Taktfrequenz arbeitet. Dieser verfügt über 6 Analogeingänge, die von drei Mini-Kreuzknüppeln angesteuert werden. Aus Kompatiblitätsgründen bekam die Anlage sieben Kanäle. 6 echte Analogkanäle und ein Kanal, mit dem mittels vier Taster Schaltimpulse gegeben werden können.

Der Stromlaufplan der Schaltung

(zusätzliche Trimmpotis nicht gezeichnet):

Der Mega48 wird mit internem Takt von 1MHz betrieben (Auslieferungszustand, 8MHz intern und aktivierte CKDIV8-Fuse). Sollte das auf die Dauer zu ungenau sein, lässt sich immer noch ein Quarz nachrüsten, die dazu benötigten Anschlusspins wurden nicht benutzt. Um wegen Batteriebetrieb Energie zu sparen werden die Potis nicht direkt an die Versorgungsspannung gelegt, sondern an den Impulsausgang des jeweiligen Kanals und GND. An einem Poti liegt also nur dann Spannung an, während es abgefragt wird. Somit fließt immer nur durch ein Poti Strom. Als Potis werden Mini-Kreuzknüppel verwendet, die einen Widerstand von 10k haben. Diese Potis haben einen eingeschränkten Widerstandsbereich (60°), liefern also am ADC die Werte von 0 bis 1023. Allerdings sind die Werte in Mittelstellung nicht exakt 511, wodurch eine Trimmung nötig wird, die mit (im Stromlaufplan nicht eingezeichneten) parallel geschalteten Trimmpoti 22k realisiert wird, dessen Stellbereich mechanisch (durch das Gehäuse) auf die mittleren 60..90° begrenzt wird. Die Kanalimpuls-Ausgänge werden über Widerstände 1k (naja, 910 Ohm, da ich davon eine Rolle in SMD habe) nach außen geführt, um Servos oder andere RC-Baugruppen zu Testzwecken direkt anschließen zu können.

Für Kanal 7 werden an PB2..PB5 vier Taster gegen GND angeschlossen. Mit diesen lassen sich (zum Neutralwert von 1,5ms) Impulsverkürzungen/verlängerungen von 0,16ms und 0,32ms (zusammen 0,48ms) erreichen. Das ermöglicht 7 Servostellungen (mit Neutralstellung) oder bei geeignetem Schaltdecoder 4 oder 6 Schaltkanäle. Für die Taster werden die internen PullUps verwendet, die nur während der Abfrage aktiviert werden um Strom zu sparen.

Ein weiterer Taster an PB1 dient zum Abschalten des Gerätes. Wird er betätigt, werden alle Komponenten des AVRs deaktiviert und der Sleep-Mode "Power Down" eingeschaltet. Zum Einschalten wird ein Taster am Reset-Pin benutzt.

Eine LED an PB0 signalisiert durch kurzes Aufblitzen, dass das Gerät eingeschaltet ist. Eine Anzeigeperiode besteht aus 16 Lichtblitzen innerhalb von 5,12 Sekunden. Diese Lichtblitze können extrem kurz (0,5ms) oder recht lang (160ms) sein, die Anzahl der langen (deutlich helleren) Blitze gibt Auskunft über den Akkuzustand. Ein langer Blitz pro Periode zeigt einen vollen Akku an, je leerer der Akku wird, desto öfter blitzt es lang. Bei kritischem Akkuzustand wird das Gerät ausgeschaltet (Power Down).


Der mechanische Aufbau...

...erfolgt wie bei Lochraster, die Platinen haben also hauptsächlich die Aufgabe, die Bauteile mechanisch aufzunehmen. Die "Verdrahtung" der Platinen untereinander erfolgt mittels Schaltdraht bzw. Flachbandkabel. Die Platinen sind aus doppelseitig kupferbeschichtetem Epoxyd-Basismaterial mittels Platinenflex angefertigt, die Trennlinien sind also mit der Diamant-Trennscheibe ausgeschliffen. Die zweite Seite der Platine dient als Abschirmung (Störschutz) und ist mit GND verbunden.

Die größere Platine trägt die Bedienelemente und ist mittels Abstandhalter aus Sperrholz am Deckel des Gehäuses (PVC-Koffer eines Bohrersatzes) angeschraubt. Alle Bauteile sind durch Bohrungen an der Unterseite der Platine angelötet. Jedem Kreuzknüppel-Poti ist ein Radiohm-Trimmpoti parallelgeschaltet, das mit einem Stellhebel versehen ist, der aus dem Gehäuse ragt. Wer in den Stellhebeln (auch der Kreuzknüppel) die PVC-Röhrchen von Wattestäbchen erkennt, hat aufmerksam hingeschaut. ;-) Die 6 Kurzhubtaster sind mit Verlängerungen versehen, die ursprünglich als Schutzkappe von Kanülen für Insulin-Pen dienten. Auch der Abstandhalter für die ultrahelle rote LED ist eine modifizierte Kanülen-Schutzkappe. Die LED wird über einen Widerstand von 910 Ohm betrieben, bekommt also nur 3 mA. Die Taster der Kreuzknüppel werden nicht benutzt.


Die untere Platine ist rein oberflächlich bestückt und trägt die IC-Fassung für den AVR, die SMD-Widerstände für Reset (4k7) und die Impulsausgänge (910 Ohm), sowie eine ISP-Steckbuchse, deren Anschlussbelegung sich aus dem Pinout des Mega16 (AT90S8535, Mega8535, Mega32) ergibt, also einen (undokumentierten) Quasi-Standard darstellt. Weiterhin bietet sie Platz zum Befestigen der Flachbandkabel zur oberen Platine mittels doppelseitigem Klebeband und Lasche, sowie zum Befestigen des Platinenstreifens, der die Buchsenleisten für die Testanschlüsse trägt. Neben der Platine ist ein Batteriehalter angebracht, in dem 4 NiCD- oder NiMH-Akkus Platz haben. Auf einen Batterieschalter wurde vorerst verzichtet, sollte er erforderlich werden, findet sich sicherlich noch Platz auf der Platine. Sollte sich mal ein geeignetes (und für RC-Fernsteuerungen zugelassenes) HF-Modul finden, dann findet sich in dem Gehäuse sicherlich auch noch ein Plätzchen. Das Bild oben rechts zeigt die Buchsenleisten von außen. Jeder dieser 8 "Steckplätze" hat die Anschlussbelegung: Impuls (links), Plus (Mitte), GND (rechts). Der linke Anschluss liefert das Signal, in dem alle Kanäle zusammengefasst sind, also das, das dem Sendertastsignal entspricht bzw. am Empfänger aus dem Demodulator kommt. Dann folgen von links nach rechts die Anschlüsse für Kanal 1 bis Kanal 7 in aufsteigender Reihenfolge. Zum testweisen Anschluss von Originalservos (mit Buchsenleiste am Anschlusskabel) werden Adapter mit beidseitigen Stiftleisten benutzt, diese Einschränkung nehme ich aber gern in Kauf, denn ich wollte an der Außenseite das Gehäuses keine offenen Stiftleisten einsetzen.


Software:

Das Programm für den Mega48 ist mittels AVR-Studio in Assembler geschrieben. Der Programmablauf ist in etwa so:
  • In der Initialisierung (Reset-Routine) wird/werden:
    • der Stackpointer initialisiert,
    • die Ports initialisiert,
    • einige Variablen vordefiniert,
    • alle nicht benötigten Features deaktiviert (Strom sparen),
    • definierte Startbedingungen für Tastenentprellung und ADC-Mittelwertberechnung (Akku-Zustand) geschaffen,
    • Timer1 mit CompareA und CompareB initialisiert (erste Int-Zeit, Int freischalten),
    • Sleep-Mode "Idle" vorgewählt.

  • In der Mainloop (Hauptschleife) wird/werden:
    • sämtliche Komponenten deaktiviert und Sleep-Mode "Power-Down" vorgewählt, falls der Aus-Taster (entprellt) betätigt wurde (auch die Unterspannungsabschaltung setzt lediglich das Austaster-Bit),
    • der Controller in den Sleep-Mode geschickt.

  • Timer1, CompareA liefert alle 20ms einen Interrupt, der als Telegrammbeginn genutzt wird. Hier wird
    • der nächste Timer1, CompareA-Interrupt in 20ms vorbereitet,
    • Impuls1 (und damit auch Poti 1) und der Sendertastimpuls eingeschaltet,
    • ADC auf Poti 1 als Signalquelle eingestellt und eingeschaltet (auf H gelegt),
    • der nächste Interrupt für Timer1, CompareB in 0,5ms vorbereitet,
    • die Tasten eingelesen und entprellt (abgespeckte Entprellung nach Peter Dannegger),
    • der Blinktakt für die LED erzeugt.

  • Timer1, CompareB liefert eine Interruptfolge zur Steuerung der Kanalimpulse innerhalb des Telegramms. Anhand des Zustandes des Sendertastimpulses wird erkannt, ob es sich um das Ende des Sendertastimpulses (0,5ms nach Impulsbeginn) handelt, oder um das Impulsende.

    • 0,5ms nach Impulsbeginn wird:
      • der Sendertastimpuls auf L gelegt,
      • die LED ausgeschaltet, falls nur der kurze Blitzimpuls ausgegeben werden soll,
      • der ADC im Interrupt-Betrieb mit Vorteiler 8 (125kHz) gestartet, da jetzt der Sender nicht syendet und keine Störungen am ADC verursacht. Das Auslesen des ADC erfolgt im ADC-Complete-Interrupt.

    • Am Impulsende (1,0ms..2,0ms nach Impulsbeginn) wird:
      • das Impulsmuster (nur eine "1" im Byte) um 1 Bit nach links (oben) geschoben und mit eingeschalteten Bit 0 (Sendertastimpuls) am Impulsport ausgegeben (also der nächste Kanal aktiviert). Dies schaltet auch das nächste Poti an Betriebsspannung.
      • die entsprechende ADC-Quelle für den jetzt beginnenden Impuls (Poti des aktuellen Kanals bzw. Bandgap-Referenz bei Kanal 7) in ADMUX eingetragen,
      • am Ende von Kanal 7 anhand des Verhältnisses Bandgap-Referenz zu Betriebsspannung (Akku) die Anzahl der langen Blinkimpulse zur Anzeige des Akkuzustandes ermittelt, sowie bei Überschreiten des Grenzwertes ein Fehlerzähler erhöht, der das Gerät bei Erreichen der vordefinierten Fehleranzahl ausschaltet (Power-Down).
  • Im ADC-Complete-Interrupt wird:
    • die erste Messung verworfen und der ADC erneut gestartet,
    • die zweite Messung ausgelesen und bei Kanal 7 als Akkuzustand in eine Mittelwertberechnung über 256 Messungen aufgenommen, bei Kanal 1..6 der ADC-Wert zur Kanalimpulsberechnung weiterverwendet,
    • bei Kanal 7 anhand des entprellten Tastenzustandes ein Ersatzwert (statt ADC-Wert) berechnet,
    • der Zeitstempel des letzten CompareB-Interrupts eingelesen und anhand ADC-Wert oder Ersatzwert die Dauer des aktuellen Impulses berechnet und danach der Zeitpunkt des nächsten CompareB-Interrupts festgelegt,
    • der ADC nach der zweiten Messung wieder deaktiviert.

Aus zeitlicher Sicht (Kanalimpulstelegramm) gesehen erfolgen die Schritte:
  • Telegrammbeginn (alle 20ms), ausgelöst durch Compare1A-Interrupt:
    • Impuls und Sendertastimpuls setzen,
    • ADC-Messquelle auf Kanal 1 (ADC5) setzen, ADC-Referenz auf Betriebsspannung setzen, rechtsbündige Ausgabe wegen 10-Bit-Ergebnis einstellen,
    • Termin für nächsten Telegrammbeginn (in 20ms über Compare1A) setzen,
    • Termin für Sendertastimpulsende (in 0,5ms über Compare1B) setzen,
    • Taster einlesen und entprellen (abgespeckte "Peter Dannegger-Methode").

  • Ende Sendertastimpuls (0,5ms nach Kanalimpulsbeginn), ausgelöst durch Compare1B-Interrupt:
    • Sendertastimpuls ausschalten,
    • LED ausschalten, falls kurzer Lichtblitz erfolgen soll,
    • ADC einschalten, falls noch ein Kanal aktiv ist, Messung erfolgt im ADC-Complete-Interrupt.

  • Erste ADC-Messung (0,62ms nach Impulsbeginn), ausgelöst durch ADC-Complete-Interrupt:
    • ADC erneut starten, erstes Messergebnis ignorieren.

  • Zweite ADC-Messung (0,74ms nach Impulsbeginn), ausgelöst durch ADC-Complete-Interrupt:
    • ADC-Messwert einlesen, ADC deaktivieren,
    • bei Kanal 7 als Spannungswert mit Mittelwert verrechnen,
    • bei Kanal 1..6 als Impulsbreitenwert (10 Bit, in µs) betrachten,
    • bei Kanal 7 anhand der (entprellten) Taster einen Ersatzwert berechnen,
    • den Zeitstempel des letzten Compare1B-Interrupt einlesen, einen Korrekturwert und den ermittelten Impulsbreitenwert addieren, um auf eine Gesamtimpulsbreite von 1..2ms (0,988..2,011ms) zu kommen, und diesen Wert als Termin für den nächsten Compare1B-Interrupt (0,988..2,011ms nach Impulsbeginn) zu setzen.

  • Kanalimpulsende (0,988..2,011ms nach Impulsbeginn), ausgelöst durch Compare1B-Interrupt:
    • alten Kanalimpuls auf L setzen, neuen Kanalimpuls (und damit das Poti) auf H setzen, Sendertastimpuls auf H setzen (Sender ein),
    • Zeitstempel dieses Interrupts einlesen, Sendertastimpulsbreite (0,5ms) dazuaddieren und als Termin für Compare1B-Interrupt setzen,
    • ADC-Messquelle auf nächsten Kanal (fallende ADMUX-Werte) einstellen, kommt Messquelle (ADC5..ADC0) unter 0 (das ist dann Kanal 7), dann interne Bandgap-Referenz (1,1V) linksbündig (8-Bit) messen um den Akkuzustand zu ermitteln,
    • nach Kanal 7 (zu Beginn der Synchronpause) Blinkzähler für LED erhöhen, bei Überlauf (alle 5,12s) die Anzahl der hellen Blinkimpulse aus dem Mittelwert des Akku-Zustandes (Bandgap-Referenz von 1,1V gegen Akkuspannung (Betriebsspannung) gemessen) ermitteln,
    • nach Kanal 7 Errorzähler erhöhen, falls Blinkanzahl das Limit überschreitet, gedrückten Aus-Taster simulieren, falls Errorzähler sein Limit erreicht.


Die Überwachung des Akkus:

Wenn Kanal 7 an der Reihe ist, braucht kein Poti ausgelesen werden, da nur 6 Analogeingänge verfügbar sind und Kanal 7 über Taster gesteuert wird. Dafür wird während Kanal 7 mit 8 Bit Auflösung die interne Bandgap-Referenzspannung von 1,1V eingelesen. Als Referenzwert dient hierbei die Versorgungsspannung des AVRs, also die zu überwachende Akkuspannung. Im Falle eines sehr vollen Akkus beträgt die Versorgungsspannung 5,2V, bei einem leeren Akku nur noch 4,4..4,0V. Die vom ADC ermittelten (8-Bit-) Werte für die 1,1V-Bandgap-Spannung betragen etwa:

5,2V = 54     (256 / 5,2V * 1,1V = 54,15...)
5,0V = 56     (256 / 5,0V * 1,1V = 56,32)
4,8V = 59     (256 / 4,8V * 1,1V = 58,67...)
4,6V = 61     (256 / 4,6V * 1,1V = 61,22...)
4,4V = 64     (256 / 4,4V * 1,1V = 64.00)
4,2V = 67     (256 / 4,2V * 1,1V = 67,05...)
4,0V = 70     (256 / 4,0V * 1,1V = 70,40)
3,8V = 74     (256 / 3,8V * 1,1V = 74,10...)
3,6V = 78     (256 / 3,6V * 1,1V = 78,22...)
Die Werte können in der Praxis etwas abweichen, die interne Bandgap-Referenz ist zwar recht konstant, unterliegt aber ziemlichen Exemplarstreuungen. Die Differenz zwischen vollem Akku (kleine Zahl) und leerem Akku (große Zahl) ist kleiner als 16. Die LED gibt in einem Anzeigezyklus (ein Umlauf des Blinkzählers) 16 Blinkimpulse aus. Durch eine Subtraktion eines festen Betrages (bei meinem Exemplar vom Mega48 ist der Betrag 60) vom ADC-Messwert und Begrenzung des Ergebnisses auf 1..15 erhält man einen Zahlenbereich, den man direkt als Anzahl der Blinkimpulse pro Zyklus verwerten kann und der als Akku-Zustand gewertet werden kann. Wenig Blinken entspricht einem vollen Akku, nerviges Vielblinken einem leeren Akku. Damit man bei einem vollen Akku nun nicht 5s die LED beobachten muss, um festzustellen, ob das Gerät eingeschaltet ist, werden alle 16 Blinkimpulse des Zyklus angezeigt, die "Leerimpulse" aber nur sehr kurz (0,5ms), die "Anzeigeimpulse" dagegen 160ms lang, man kann den Unterschied sehr deutlich erkennen.

Im Kopf des Quelltextes gibt es einige Konstanten zum Kalibrieren von Anzeige und Abschaltung bei kritischem Akkuzustand:

.equ akkudiff=60        ;Differenz, um von Bandgap/Vcc (54..78) auf
                        ;"Blinkpunkte" für Akkuzustand (1..15) zu kommen 
.equ notaus=12          ;Grenzwert Akkuzustand (8..15 Blinkpunkte), bei dem
                        ;der Unterspannungsfehlerzähler erhöht wird
.equ fehler=20          ;Anzahl der Unterspannungsfehler, bei
                        ;der AVR in den Power-Down-Mode geschickt wird
Somit lässt sich die Spannungsüberwachung an die Exemplarstreuungen und die Entladekennlinie des Akkus (Abschaltpunkt) anpassen.

Um Störung der Spannungsmessung zu vermeiden, erfolgt eine Mittelwertbildung über 256 Messungen. Dies geschieht, indem bei jeder Messung zuerst 1/256 des aktuellen Mittelwertes aus dem Mittelwert subtrahiert wird und danach 1/256 des Messwertes zum Mittelwert addiert wird. Als Startwert wird die Konstante "akkudiff" genutzt. Der Bruch "1/256" wird durch Byteshiften erreicht. Um 1/256 des Mittelwertes zu subtrahieren wird einfach der Betrag des Highbytes vom Lowbyte subtrahiert und dann ein eventueller Übertrag (Carry) vom Highbyte subtrahiert:

 sub volt0,volt             ;1/256 des Mittelwertes (mit Übertrag)
 sbc volt,null              ;vom Mittelwert subtrahieren
Wobei "volt" das Highbyte ist, "volt0" das Lowbyte (Nachkommastellen) und "null" immer 0 enthält. Um 1/256 des neuen Messwertes (in "xh") zum Mittelwert zu addieren, wird der Wert zum Lowbyte addiert und ein evtl. entstehender Übertrag ins High-Byte.
 add volt0,xh               ;1/256 des ADC-Wertes (mit Übertrag) zum
 adc volt,null              ;Mittelwert addieren (Akkuspannungsmessung)
Diese Methode ist sehr ressourcensparend und benötigt zur Ausführung nur 4 Takte.
Die Auflösung der Kanalimpulsbreite

Die gesamte Zeitsteuerung arbeitet mit Timer1, der mit 1MHz läuft. Alle Zeiten liegen daher im Raster von 1µs. Die Potistellungen der Kreuzknüppel werden in 10-Bit-Auflösung gemessen (0..1023) und direkt zum "Basisimpuls" von 988 Takten addiert. Die Impulsbreite eines Kanals kann also 1024 verschiedene Werte annehmen. Einige Eckwerte der Impulserzeugung lassen sich im Kopf der ASM-Datei (als Konstanten definiert) an die Gegebenheiten anpassen:

.equ ibs=500            ;Impulsbreite Sendertastimpuls in µs
.equ iab=20000          ;Impulsabstand in µs
.equ korr=1500-512-ibs  ;Korrekturdifferenz zwischen Sendertastimpuls (0,5ms)
                        ;und min. Kanalimpuls (1,5ms - 0,512ms)

© 02/2006 by hannes@hanneslux.de