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.
bitfolge als Diagramm

Realisierung

Schaltplan

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
Bild mit Codezuordnung zu den Tasten

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(){
}