Fischertechnik
AVR
Raspberry Pi
Elektronik
Netzwerk
Sonstiges
Impressum
|
Sleep-Funktion für Stereoanlage
Die Stereoanlage TCM231421 verfügt über Radio-, Cassetten-, CD-,
MP3- und USB-Stick-Unterstützung und lässt sich per
Infrarot-Fernbedienung vollständig und bequem bedienen.
Einzig die fehlende Sleep-Funktion unterscheidet diese günstige
Anlage von deutlich teureren, über den Klang mag ich nicht urteilen.
Diese Funktion lässt sich ohne großen Aufwand und ohne Eingriff
in das Gerät nachrüsten. Die Idee ist einfach: Eine kleine
Schaltung erzeugt nach einer Wartezeit von z.B. 30 oder 60 Minuten ein
Infrarot-Signal, wie die Fernbedienung beim Ausschalten. Als besonderen Komfort
wird vorher die Lautstärke heruntergeregelt.
Hierzu muss das Signal der Fernbedienung bekannt sein oder analysiert werden.
Letzteres habe ich mit einem Arduino und einigen Zeilen Code durchgeführt.
Das IR-Signal hat eine Trägerfrequenz von 38 kHz.
Die Modulation ist wie bei Fernbedienungen üblich sehr einfach, es gibt
nur an und aus. "An" gibt es in zwei unterschiedlichen Dauern, "Aus" in sieben.
Nach jedem Tastendruck wird das Licht lange (ca. 9,1 Millisekunden)
eingeschaltet. Die folgende Pause dauert dann ca. 4,4 Millisekunden; es folgen
32 Datenbits, die jeweils aus kurz "an" (0,65 Millisekunden) und zwei
unterschiedlich langen Pausen bestehen. Die kurze Pause dauert ca. 0,46 Millisekunden, die lange 1,6; ersteres dekodiere ich als binäre 0,
letzteres als binäre 1. Damit die letzte Pause vom Empfänger
dekodiert werden kann, folgt noch ein kurzes "an". Wurde die Taste nur kurz
gedrückt, ist jetzt Schluss, ansonsten folgt nach ca. 40 Millisekunden
erneut ein langes "an" gefolgt von 2,2 Millisekunden Pause gefolgt
vom obligatorischen Abschlusspuls. Bleibt die Taste weiterhin gedrückt,
wiederholt sich der eben beschriebene Block nach ca. 96,4 Millisekunden.
Die 32 Datenbits entsprechen 4x8 Bit, wobei die ersten 8 Bit den Wert
00000000 und die zweiten den Wert 11111111 haben. Die vierten 8 Bit
entsprechen immer den bitweise invertierten dritten 8 Bit.
Als Tastencode habe ich immer die dritten 8 Bit in hexadezimaler
Darstellung angegeben. Der Code 0A (Power) lautet in binärer
Schreibweise 00001010 und ist von links nach rechts zu interpretieren. Die
Fernbedienung schickt, also zuerst vier Lichtpulse mit kurzer Pause, dann
einen mit langer Pause, dann wieder kurz, dann wieder lang und zum Abschluss
mit kurzer Pause. Für dieses Beispiel die vollständige
Bitfolge: 00000000111111110000101011110101.
Realisierung
Timer 1 erzeugt die Trägerfrequenz
In den ersten Versuchen habe ich die tone-Bibliothek genutzt, um die
Trägerfrequenz zu erzeugen und habe mich dann gewundert, dass die Funktion
delayMicroseconds ca. Faktor 2 zu langsam war. Die Ursache liegt darin,
dass die Bibliothek Interrupt-gesteuert arbeitet und 38 kHz dann einfach
zu viel sind. Die Hälfte der Rechenleistung wird in der IR-Routine
verbraten.
Timer 0 ist im Arduino für die Zeit im erweiterten Sinne zuständig und
somit nicht frei zur Nutzung. Somit habe ich Timer 1 mit dem Ausgang OC1A zur
Erzeugung der Trägerfrequenz genutzt. Die IR-Sendedioden (zwei in Serie)
arbeiten gegen Masse; die Modulation erfolgt durch das DDRB: Wird Bit 1
gesetzt, wird die Sonderfunktion OC1A auf den Pin 6 geschaltet; wird
dieses Bit zurückgesetzt, ist Pin 6 ein Eingang und stromlos.
void init_timer1(){
TCCR1 = B10000000 | B00010000 | B00000001;
OCR1A = 104; // 38 kHz aus 8MHz;
OCR1C = 104;
TIMSK &= B10011011; // alle Timer-1-Interrupts aus
}
#define IRon DDRB |= B00000010 // B1 als Ausgang=OC1A
#define IRoff DDRB &= B11111101 // B1 als Eingang
Restzeitanzeige
Als q'n'd-Lösung lasse ich jede volle Minute eine zusätzliche LED
je einmal pro Restminute blinken.
Abschalten...
Nach Ablauf des Programms wird der ATtiny schlafen gelegt. Der Stromverbrauch
sinkt auf gemessene 220 μA. Dieses ist zwar deutlich unterhalb
des Verbrauchs im Normalbetrieb, mir aber noch deutlich zu hoch. Da ist wohl
noch ein Blick ins Datenblatt fällig...
Details
Messung der Trägerfrequenz
Der erste Versuch, die Trägerfrequenz zu messen ist schief gegangen.
Ich habe gleich mehrere Fehler gemacht: Faul wie ich war, habe ich den
internen Pullup-Widerstand genutzt. Zusammen mit der Sperrschichtkapazität
des Fototransistors ergibt sich ein Tiefpass mit einer Grenzfrequenz unterhalb
der Trägerfrequenz. Besser geht es mit einem 1 kΩ-Widerstand.
Das nächste Problem ist die unzureichende Auflösung der Zeitmessung.
Der Name der Routine micros() stimmt nur bezüglich der
Einheit, in der gemessen wird; die Auflösung beträgt bestenfalls
4μs, was bei der Messung über mehrere Taktperioden dann durchaus
brauchbar ist. Typische Trägerfrequenzen sind 36, 38 und 40 kHz, was
den Periodendauern 27,7 μs, 26,3 μs und 25 μs
entspricht.
Da die Pulsfolgen bei vielen Fernbedienungen oft nicht mehr als 10 Pulse
beträgt, ist das Mitteln über viele Pulse nicht sinnvoll; entsprechend
ungenau ist die Messung. Die dann doch ganz einfache Lösung dieses Problems
ist das Auszählen der Taktzyklen einer einfachen Schleife.
Mit der folgenden Routine unterscheiden sich die Ergebnisse für die o.a.
Trägerfrequenzen recht deutlich: 176 für 36 kHz,
166 für 38 kHz und 157 für 40 kHz. Die Angabe einer
Nachkommastelle erscheint bei diese Messmethode gerade noch angemessen.
int period(){ // Arduino-Anschluss 12 ist PINB4
int cnt=0;
byte pb;
while((PINB & B00010000)==0); // warte auf dunkel
while((PINB & B00010000)!=0); // warte auf hell
while((PINB & B00010000)==0)cnt++; // warte auf dunkel
while((PINB & B00010000)!=0)cnt++; // warte auf hell
while((PINB & B00010000)==0)cnt++; // warte auf dunkel
while((PINB & B00010000)!=0)cnt++; // warte auf hell
return cnt;
}
float frequenz(int perioden){
long f=2*160000/(12+5*perioden);
return(f/10.0);
}
Der Assemblercode der unteren vier Schleifen ist bis auf die
Schleifenbedingung immer gleich und benötigt 5 Zyklen; der restliche
Overhead, der in die Messung eingeht beträgt 12 Zyklen (Messung über
zwei Zyklen).
1ba: 2f 5f subi r18, 0xFF ; 255
1bc: 3f 4f sbci r19, 0xFF ; 255
1be: 1c 9b sbis 0x03, 4 ; PINB4
1c0: fc cf rjmp .-8 ; 0x1ba
Messung des demodulierten Signals
Das demodulierte Signal hat typischerweise keinen Frequenzanteil oberhalb 1/10
der Trägerfrequenz und kann mit Arduino-Bordmitteln gemessen werden. Im
konkreten Fall liegt die kürzeste zu messende Zeit bei 460 μs
und damit weit über der Auflösungsgrenze.
Weitere Ideen
Für Selbstbauprojekte stellt sich immer wieder die Frage nach einem
passenden Gehäuse und den Bedienelementen. Hier reicht eigentlich
eine einzige Taste, eine Loch für die IR-Sendediode und ggf. eine
Ladebuchse für den LiPo. Für variable Ausschaltzeiten reicht das
aber nicht.
Eine recht einfache Lösung ist die Bedienung mit der Original
Fernbedienung. Einmal Taster(=Reset) an der Schaltung drücken, und
dann die Dauer z.B. mit einer der Tasten 0 bis 9 auf 10, 20, 30, 45
... 120 Minuten festlegen. Hierzu werden zwei weitere Ports benötigt,
wenn die Stromversorgung des IR-Empfängers über einen Port erfolgt.
Der Taster kann durch einen Rüttelschalter ersetzt werden, dann spart
man sich auch diesen Einbau.
Radiowecker
Nach der Implementierung und Bewährung der Sleep-Funktion liegt es nahe,
auch eine Weckfunktion zu entwickeln. Diese erfordert einerseits eine
solide Uhrzeit und andererseits auch eine echte Anzeige. Ersteres geht
entweder über DCF77 oder GPS oder mit einem speziellen Uhrenchip; wer
beides nicht möchte, kann auch einen Uhrenquarz an den ATtiny/ATmega
anschließen.
Benötigte Anschlüsse:
- Masse, Versorgungsspannung, Reset
- I2C oder Rx und Tx für Zeit (oder XTAL1, XTAL2)
- Uhren-Chips wie z.B. der DS1337+ haben oft die Möglichkeit
per Interrupt zu "wecken".
- LCD: D4-D7, E, R/W, RS
- Infrarot-Sendediode
- Taster zum Stellen; z.B. Drehencoder mit Taster. Die
(drei) Anschlüsse
lassen sich mit den LCD-Anschlüssen D4-D7 gemeinsam nutzen.
Minimallösung wäre z.B. ein ATtiny24/44/84 im 14-poligen Gehäuse.
VCC 1 14 GND
LCD RS PB0 2 13 PA0 D4 LCD + Drehencoder A
LCD R/W PB1 3 12 PA1 D5 LCD + Drehencoder B
Reset 4 11 PA2 D6 LCD + Drehencoder Taster
I2C INT0 5 10 PA3 D7 LCD
LCD E PA7 6 9 SCL I2C
I2C SDA 7 8 OC1B IR
|
|
Quelltext
#include <avr/sleep.h>
// attiny45
//
// Reset 1 8 +3,7V
// Pin 3 2 7 Pin 2 Analog 1 SCK
// Pin 4 3 6 Pin 1 PB1 OC1A MISO
// GND 4 5 Pin 0 PB0 PWM MOSI
//
// Ausgangsstrom 40mA bedeutet ca. 1,2 V weniger Spannungshub als Versorgungsspannung
// Diode+Vorwiderstand also 3 Volt bei max. Batteriespannung; davon 2*1,35 V Diode
// Vorwiderstand=0,3V/0,04A=7,5 Ohm; 10 Ohm sollte "safe" sein...
//
// Timer 0 ist mit delay etc. belegt
// Timer 1 wird fuer PWM initialisiert, sollte aber nutzbar sein
//
// 38kHz aus 8MHz ist Teiler von ca. 210,5 lfuse 0xE2
// 38kHz aus 1MHz ist Teiler von ca. 26,3 lfuse 0x62
//
// 8MHz FUSES = -U hfuse:w:0xDD:m -U lfuse:w:0xE2:m
//
// Clear Timer on Compare Match (CTC) Mode loescht bei Erreichen
// des Wertes im Register OCR1C.
// Toggle-Mode: COM1A=01 sorgt dafuer, dass bei jedem Zyklus der Ausgang invertiert
// wird, also ein symmetrisches (square-wave) Ausgangssignal erzeugt wird.
// Frequenz: f_io/(2*N*(1+OCR1C) N=prescaler 1/8/64/256 oder 1024
// CTC1 COM1A Takt an ohne weiteren Vorteiler
void init_timer1(){
TCCR1 = B10000000 | B00010000 | B00000001;
OCR1A = 104; // 38 kHz aus 8MHz;
OCR1C = 104;
TIMSK &= B10011011; // alle Timer-1-Interrupts aus
}
#define IRon DDRB |= B00000010 // B1 als Ausgang=OC1A
#define IRoff DDRB &= B11111101 // B1 als Eingang
#define LEDon DDRB |= B00000001 // B0 als Ausgang
#define LEDoff DDRB &= B11111110 // B0 als Eingang
#define StartPuls { IRon;delay(9);delayMicroseconds(100);IRoff;}
#define NormalPuls { IRon;delayMicroseconds(650);IRoff;}
#define Bit0Pause delayMicroseconds(450)
#define Bit1Pause delayMicroseconds(1600)
#define StartPause delayMicroseconds(4400)
#define WiederPause delayMicroseconds(2200)
void WarteMinuten(byte m){
byte i,j,k;
for(i=m;i>0;i--){
for(j=0;j<i;j++){
LEDon;
delay(50);
LEDoff;
delay(200);
};
delay(45000L+(60-i)*250);
};
}
#define LEISER 0xba
#define POWER 0x0a
void SendByte(byte c){
byte i,j,k;
k=c;
j=B10000000;
for(i=0;i<8;i++){
NormalPuls;
if( (k&j)!=0 ) Bit1Pause; else Bit0Pause;
j=(j>>1);
};
}
void SendCode(byte c){
StartPuls;
StartPause;
SendByte(0);
SendByte(~0);
SendByte(c);
SendByte(~c);
NormalPuls;
}
void Wiederholen(byte anz){
for(byte i=0;i<anz;i++){
delay(40);StartPuls;WiederPause;NormalPuls;delay(56);
};
}
void setup(){
WarteMinuten(30);
init_timer1();
IRoff;
delay(2000);
for(int i=0;i<30;i++){
SendCode(LEISER);Wiederholen(1);
delay(1000);
};
SendCode(POWER);Wiederholen(30);
delay(10000);
SendCode(LEISER);Wiederholen(30);
SendCode(LEISER);Wiederholen(30);
DDRB=0; // Alle Ausgaenge abschalten
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sleep_enable();
sleep_mode();
}
void loop(){
}
|