Dies ist eine alte Version des Dokuments!


IR-Empfänger oder Puls abstand/breiten Demodulierer

Auch wenn der Titel etwas abschreckend klingt, geht es hier um eine recht einfache Sache. Viele einfache Funksender, wie TV Fernbedienungen, senden die Daten binär in der Form eines Pulses, wobei entweder die breite des Pulses oder der zeitliche Abstand zwischen zwei Pulsen entscheidet, ob eine 1 oder eine 0 gesendet wurde.

Grundlagen

Daten über Funk zu senden ist immer eine Herausforderung. Man muss mit Störungen und Interferenzen rechnen, mann muss seinen Sender so gut konstruiert haben, dass er möglichst nur im zugeteilten Frequenzbereich sendet und das ganze hat auch noch klein und günstig genug zu sein, dass man es sich leisten kann. Damit fallen viele Lösungen weg, die Daten auf das Trägersignal Aufmodulieren indem Amplitude, Frequenz oder beides geändert wird. Übrig bleibt das detektieren ob das Trägersignal zu empfangen ist oder nicht. Diese Lösung ist sehr einfach und lässt sich mit wenig Aufwand realisieren. Jedoch muss man ein Protokoll nutzen, dass es erlaubt Daten einigermaßen störungssicher zu übertragen.
Dazu hat sich die Puls Abstand- beziehungsweise Breiten-Modulation etabliert. Dazu sendet man kontinuierlich Pulse, wobei man entweder die zeit zwischen den Pulsen ändert um eine 1 von einer 0 zu trennen, oder eben die Pulsbreite:
Oszillogramm Puls-Abstand-Modulation

Die gelben Spitzen sind jeweils 1 Puls, die im hier gezeigten Fall alle die selbe Dauer haben. Zu sehen sind auch die beiden Abstände T1 und T2, die eine 1 oder eine 0 darstellen. T1 ist hier ca 1ms, T2 ca 2ms.
Die Hardware ist nicht sonderlich kompliziert:
IR Receiver Hardware

Hier zu sehen ist Beispielhaft der IR-Receiver TSOP8523. Generell muss nur die Signalquelle an einen freien digitalen Eingang angeschlossen werden.

Signal Auswerten

Um nun so einen Verlauf auszuwerten, muss man auf die Änderung der Flanke reagieren und die Zeit zwischen zwei Flankenwechseln erfassen. Die Flankenerkennung könnte man per Interrupt lösen, ich habe mich aber für eine andere Methode entschieden. Dazu lasse ich einen Timer laufen, der alle 50µs einen Interrupt auslöst. Im Interrupt wird dann der entsprechende Eingangspin gepollt und ausgewertet. Der Vorteil dieser Methode ist, dass ich einfach nur einen Zähler mitlaufen lasse und die Zeit zum letzten Ereignis als Zählerwert angeben kann. Zudem lässt sich Pulsbreiten und Puls-Abstandsmodulation mit dem selben Code auswerten.
Timer Initialisierung und globale Variablen:

#define TIME_ARRAY_COUNT 300     //max amount of bits per frame
#define MIN_BITS_RECEIVE 200     //minimum amount of bits to receive
#define FRAME_TIME 200           //maximum time the frame needs to transmitt
#define LATCH_VALUE 0           //Idle state of the input
#define TIMEOUT 800              //how many counts till timeout
 
volatile byte timeArray[TIME_ARRAY_COUNT];     //data structure to safe the times between each puls
volatile int index = 0;                        //actual index in the timeArray structure, used to get the amount of received times
volatile byte received = 0;                    //state to check if transmission has completed
 
 
void setup() 
{ 
  Serial.begin(115200); 
  pinMode(3, INPUT);
  TCCR2A = (1 << WGM21);                //CTC mode
  TCCR2B = (1 << CS21) | (1 << CS20);   //Prescaler == 32
  OCR2A = 25;    //16MHz/32 => 2µs count, 50 * 2µs 100µs per interrupt
  OCR2B = 25;    //unnecessary?
  TIMSK2 |= (1 << OCIE2A);    //activate the timer interrupt
} 

Nun der Timer Interrupt:

ISR (TIMER2_COMPA_vect)
{
  static byte latch = 0;    //static variable to latch the input
  static int count = 0;     //count the time since the last event
  count++;
  //Latch the input, used to reduce errors
  latch <<= 1;
  latch &= 7;    
  latch |= (digitalRead(3) & 0x01);
  if(latch == 0b00000001)  //rising edge
  {
    timeArray[index] = count;
    index++;
  }
  else if(latch == 0b00000110)  //falling edge
  {
    count = 0;    //reset count
  }
  else
  { 
    // Receive condition: 
    // received enough bits and waited long enough to be sure there
    // is not any data following  
 
    if(index > MIN_BITS_RECEIVE && latch == LATCH_VALUE && count > FRAME_TIME)   
    {
      received = 1;
      count = 0;
    }
    else if(count >= TIMEOUT)   // Timeout
    {
      index = 0;
      count = 0;
      received = 0;
    }
  }
}

Sobald received auf 1 gesetzt wird, haben wir ein Array an Zeiten, wie viel Zeit zwischen den einzelnen Frames liegt. Dies auszuwerten ist nicht sehr kompliziert:

void loop() 
{
  static uint8_t data[4];
  int tmp = index; 
  int state = 0;
  if(received == 1)
  {
    received = 0;
    index = 0;
    Serial.print("Index: ");
    Serial.println(tmp);
    Serial.println("Time Array:");
    for(int i = 0; i < TIME_ARRAY_COUNT; i++)
    {
      Serial.print(timeArray[i]);
      Serial.print(", ");
    }
    Serial.println("");
  }
}

Diese Funktion gibt die einzelnen Zeiten über die serielle Schnittstelle zurück, was sehr hilfreich ist wenn man ein unbekanntes Protokoll verstehen will.
Häufig sind aber neben den Bits auch noch Header dabei, die sich einfach durch eine deutlich längere Zeit absetzten. Hier muss man leider noch selber eine Auswertung schreiben.

Auswertung IR-Fernbedienung "Falcon-X"

Der kleine RC-Helikopter „Falcon X“ ist ein sehr beliebtes Spielzeug und Geschenk, wer einen besitzt ist also daran interessiert, die Fernbedienung weiter zu verwenden. Die Fernsteuerung nutzt die schon beschriebene Puls-Abstands-Modulation um die Daten zu senden. Die Auswertung baut auf der schon beschriebenen Erfassung der Zeiten auf. Die Parameter sind folgende:

#define TIME_ARRAY_COUNT 40     //max amount of bits per frame
#define MIN_BITS_RECEIVE 20     //minimum amount of bits to receive
#define FRAME_TIME 60           //maximum time the frame needs to transmitt
#define LATCH_VALUE 7           //Idle state of the input
#define TIMEOUT 100              //how many counts till timeout


Das Protokoll an sich sendet erst einen Header, der größer als 55 counts ist. Danach werden 4 Bytes, also 32 Bit gesendet. Die Zuordnung der Bits zu den Kontrollen der Fernbedienung ist folgende:

Byte Bits Funktion
1 0..6 Throttle/Gas
1 7 AUX An/Aus
2 0..3 Hoch/Runter
2 4..7 links/rechts
3 0..4 Trimm-Poti
3 5 Trimm Rechts, wenn gesetzt, ansonsten links
3 6 Runter wenn gesetzt, Hoch wenn nicht
3 7 Links wenn gesetzt, Rechts wenn nicht
4 0..5 Heckrotor Geschwindigkeit(2er Komplement?)
4 6 immer aus(Kennung?)
4 7 immer an(Kennung?)

Die Auswertung findet in der loop-Funktion statt:

 static uint8_t data[4];
  int tmp = index; 
  if(received == 1)
  {
    received = 0;
    index = 0;
    if(timeArray[0] < 55)   //Header is wrong
    {
      return;
    }
    for(int i = 0; i < tmp; i++)        //extract the data from the times
    {
      data[i >> 3] <<= 1;
      data[i >> 3] |= (timeArray[1 + i] < 7)?(1):(0);   //Times smaller 7 are a 1, higher values are a 0
    }
    //Demo application, use the values to controll servos
    servo3.write((data[0] & 0x7F) + 30);   //throttle
    servo4.write(94 - ((data[1] >> 4)*((data[2] & (1 << 7))?(-1):(1)))*2);    //rudder
    servo2.write(92 - ((data[1] & 0x0F)*((data[2] & (1 << 6))?(-1):(1)))*2);   //elevation
    servo1.write((data[0] & (1 << 7))?(120):(180));
  }
}