Kampis Elektroecke

IR-Empfänger für AVR

In diesem Artikel möchte ich zeigen, wie ein TSOP38238 IR-Empfänger zusammen mit einem AVR (oder einem anderen Mikrocontroller) genutzt werden kann um Daten einer Fernbedienung zu empfangen, um so den Mikrocontroller mittels IR-Fernbedienung zu steuern.

Grundlagen zur Kommunikation mittels Infrarot:

Für die Kommunikation mittels Infrarot werden verschiedene Verfahren kombiniert um so eine energiesparende und störungsarme Übertragung zu ermöglichen.

Für die Übertragungsstrecke (i. d. R. Luft) werden die einzelnen Informationen über eine Puls-Code-Modulation (PCM) mit einem 30 – 56 kHz Trägersignal moduliert und anschließend übertragen.

Durch die Modulation werden Übertragungsfehler reduziert und das System gegen den Einfluss von Tageslicht abgehärtet.

Bei den gesendeten Informationen handelt es sich um Daten, die in einem herstellerspezifischen Protokoll und mit einer bestimmten Codierung gesendet werden. Die Codierung bestimmt dabei, wie die Elemente einer Übertragung, in diesem Fall eine logische 0 oder eine logische 1, aussehen. Eine aufeinanderfolgende Serie von Pulsen wird Burst und eine aufeinanderfolgende Serie von Pausen wird Space genannt. 

Codierung Verlauf
Differentieller Manchester-Code
Puls-Distanz Code
Puls-Längen Code

Jede Übertragung wird mit einem Header, welcher aus einem Burst mit einer bestimmten Länge besteht, eingeleitet. Dieser Header wird für die automatische Verstärkungsregelung (Automatic Gain Control – AGC) des Empfängers genutzt wird.

Das implementierte Protokoll unterscheidet sich je nach Hersteller und viele der Funktionen sind nicht standardisiert. In diesem Artikel werde ich mich ausschließlich auf das Protokoll der Firma NEC beziehen. Für dieses Protokoll werden folgende Übertragungsparameter genutzt:

  • Puls-Längen Codierung
  • 38 kHz Trägersignal
  • Logische 0
    • 562,5 µs Burst
    • 562,5 µs Pause
  • Logische 1
    • 562,5 µs Burst
    • 1,6875 ms Pause

Dabei besteht eine komplette Nachricht immer aus den folgenden Komponenten:

  • Burst mit einer Länge von 9 ms als Header
  • Space mit einer Länge von 4,5 ms
  • 8-Bit Adresse des Empfängers
  • Invertierte Adresse
  • 8-Bit Kommando
  • Invertiertes Kommando
  • Burst mit einer Länge von 562,5 µs um das Ende einer Übertragung zu signalisieren

Für den Fall das ein Sender die gesendete Nachricht wiederholen möchte (z. B. falls bei einer Fernbedienung eine Taste länger gedrückt bleibt), wird nach dem Senden einer initialen Nachricht ein verkürzter Code, der sogenannte Repeat Code, gesendet. Dieser Code besteht aus den folgenden Komponenten:

  • Burst mit einer Länge von 9 ms
  • Space mit einer Länge von 2,25 ms
  • Burst mit einer Länge von 562,5 µs um das Ende einer Übertragung zu signalisieren

Typischerweise wird der Repeat Code 40 ms nach dem Ende der Nachricht gesendet und anschließend alle 108 µs wiederholt. In der Praxis sieht sieht das ganze dann folgendermaßen aus:

  • Gelb: Rohsignal, direkt mit von einem Photo-Transistor empfangen
  • Blau: Demoduliertes Signal aus dem IR-Empfänger

Implementierung des Protokolls auf dem Mikrocontroller:

Damit der Mikrocontroller gesendete IR-Nachrichten empfangen kann, wird ein geeigneter Empfänger benötigt. Bei der Auswahl des Empfängers ist darauf zu achten, dass der Empfänger für die verwendete Trägerfrequenz (hier 38 kHz) ausgelegt ist. Als Mikrocontroller soll ein XMega256A3BU verwendet werden, wobei der Empfänger am Pin 0 vom Port E angeschlossen wird.


Achtung:

Der Empfänger verwendet einen Open-Kollektor-Ausgang für die Datenübertragung. Dieser sorgt dafür, dass sämtliche Informationen invertiert werden!


Die Auswertung der Pulszeiten soll über den Timer D0 Timer erfolgen. Dazu wird der Timer so konfiguriert, dass dieser alle 50 µs einen Overflow-Interrupt erzeugen soll.

#define IR_TICK_INTERVAL			50

static Timer0_Config_t _IR_TimerConfig = 
{
    .Device = &TCD0,
    .Prescaler = TIMER_PRESCALER_8
};

static Timer0_InterruptConfig_t _IR_TimerInterrupt =
{
    .Device = &TCD0,
    .Source = TIMER_OVERFLOW_INTERRUPT,
    .InterruptLevel = INT_LVL_HI
};

static void _IR_Timer0OverflowCallback(void)
{
    ...
}

void IR_Init(void)
{
    GPIO_SetDirection(GET_PERIPHERAL(IR_REC_INPUT), GET_INDEX(IR_REC_INPUT), GPIO_DIRECTION_IN);

    _IR_TimerConfig.Period = F_CPU / ((((uint32_t)0x01) << (_IR_TimerConfig.Prescaler - 0x01)) * (1000000 / IR_TICK_INTERVAL)); 
    _IR_TimerInterrupt.Callback = _IR_Timer0OverflowCallback
    Timer0_Init(&_IR_TimerConfig);
    Timer0_InstallCallback(&_IR_TimerInterrupt);
    PMIC_EnableInterruptLevel(_IR_TimerInterrupt.InterruptLevel);
    EnableGlobalInterrupts();
}

In der ISR vom Overflow-Interrupt wird der Zustand des Empfängerpins eingelesen und ein Zähler inkrementiert und über einen Zustandsautomaten ausgewertet.

Zuerst wird der Zustandsautomat initialisiert, indem die Funktion IR_GetMessage aufgerufen wird:

bool IR_GetMessage(IR_Message_t* Message)
{
    _IR_MessageBuffer = Message;
    _IR_MessageBuffer->Valid = false;
    _IR_TimerTicks = 0x00;
    _IR_TimeOutTicks = 0x00;
    _IR_RecState = IR_STATE_IDLE;
    while(!_IR_MessageBuffer->Valid)
    {
        if(_IR_TimeOutTicks > (IR_TIMEOUT / IR_TICK_INTERVAL))
        {
            _IR_RecState = IR_STATE_STOP;
            _IR_MessageBuffer->Valid = false;
            return _IR_MessageBuffer->Valid;
        }
    }

    return _IR_ProcessData();
}

Die Funktion erwartet als Parameter einen Zeiger auf ein Objekt vom Typ IR_Message_t, welches folgendermaßen aufgebaut ist:

typedef struct
{
    uint8_t Length;
    union
    {
        uint8_t Field[4];
        uint32_t Value;
    } Data;
    bool Valid;
    bool IsRepeat;
} __attribute__((packed)) IR_Message_t;
Feld Funktion
Length Länge der Übertragung
Data Ein Union um die empfangenen Daten entweder als Array oder als 32-Bit Integer zur Verfügung zu stellen
Valid Flag um eine gültige Nachricht zu signalisieren
IsRepeat Flag um ein Repeat Code zu signalisieren

Wird innerhalb einer bestimmten Zeit keine gültige Nachricht empfangen, wird der Zustandsautomat angehalten, ein Timeout ausgelöst und die Funktion verlassen. Andernfalls werden die empfangenen Daten über die Funktion _IR_ProcessData weiterverarbeitet.

Direkt nach der Initialisierung befindet sich der Zustandsautomat im Zustand IR_STATE_IDLE, in dem der Automat auf eine steigende Flanke am Eingangspin wartet.

case IR_STATE_IDLE:
{
    if(InputState)
    {
        if(_IR_TimerTicks < IR_PROTOCOL_BURST)
        {
            _IR_TimeOutTicks++;					
        }
        else
        {
            _IR_MessageBuffer->Length = 0x00;
            _IR_RecState = IR_STATE_REC_SPACE;
        }
        _IR_TimerTicks = 0x00;
    }
    break;
}

Dazu wertet der Automat zusätzlich noch den Zähler aus und wenn der Zählerstand zu niedrig ist, wird der Zähler zurückgesetzt, ein Zähler für den Timeout inkrementiert und anschließend die ISR verlassen. Falls der Zählerstand den Grenzwert überschreitet, ist ein gültiger Burst erkannt worden und der Zustandsautomat wechselt in den Zustand IR_STATE_REC_SPACE.

case IR_STATE_REC_SPACE:
{
    if(!InputState)
    {
        _IR_Data[_IR_MessageBuffer->Length++] = _IR_TimerTicks;
        _IR_TimerTicks = 0x00;
        _IR_RecState = IR_STATE_REC_SPACE;
    }
    else if(_IR_TimerTicks > (IR_TIMEOUT / IR_TICK_INTERVAL))
    {
        _IR_RecState = IR_STATE_STOP;
    }
    break;
}

In diesem Zustand wertet der Zustandsautomat eine ankommenden Pause aus. Dazu wartet der Automat so lange bis eine fallende Flanke am Empfangspin auftritt. Wenn der Zählerstand einen bestimmten Wert überschreitet und der Empfangspin gesetzt ist, wird der Zustandsautomat angehalten und der Empfang beendet. Andernfalls wird der aktuelle Zählerstand im Array _IR_Data gespeichert, der Zähler zurückgesetzt und der Automat wechselt in den Zustand IR_STATE_REC_BURST

case IR_STATE_REC_BURST:
{
    if(InputState)
    {
        _IR_Data[_IR_MessageBuffer->Length++] = _IR_TimerTicks;
        _IR_TimerTicks = 0x00;
        _IR_RecState = IR_STATE_REC_SPACE;
    }
    break;
}

Dieser Zustand wertet die nachfolgende Pausenzeit eines Bits aus. Der Zustandsautomat wartet bis der Eingangspin gesetzt ist und somit eine steigende Flanke detektiert wurde. Dann wird der Zählerstand erneut im Array _IR_Data gespeichert und der Automat springt zurück in den Zustand IR_STATE_REC_SPACE.

Dieser Ablauf wiederholt sich so lange bis der letzte Burst ohne nachfolgende Pause gesendet wird. Dann springt der Zustandsautomaten aus dem Zustand IR_STATE_REC_SPACE in den Zustand IR_STATE_REC_STOP, wo ein entsprechendes Flag gesetzt wird um das Ende der Datenübertragung zu signalisieren.

case IR_STATE_STOP:
{
    _IR_MessageBuffer->Valid = true;
    break;
}

Wurde die Nachricht komplett empfangen und das Valid-Flag gesetzt, wird die Funktion _IR_ProcessData aufgerufen, wo die gespeicherten Nachricht ausgewertet wird.

static bool _IR_ProcessData(void)
{
    if(_IR_MessageBuffer->Length == 0x02)
    {
        _IR_MessageBuffer->IsRepeat = true;
        _IR_MessageBuffer->Data.Value = 0x00;
        if((_IR_Data[0x00] < ((IR_PROTOCOL_SPACE / 2) + IR_PROTOCOL_TOLERANCE)) && (_IR_Data[0x00] > ((IR_PROTOCOL_SPACE / 2) - IR_PROTOCOL_TOLERANCE)))
        {
            _IR_MessageBuffer->Valid = true;
        }
        else
        {
            _IR_MessageBuffer->Valid = false;
        }
    }
    else
    {
        _IR_MessageBuffer->IsRepeat = false;
        if((_IR_Data[0] > (IR_PROTOCOL_SPACE + IR_PROTOCOL_TOLERANCE)) || (_IR_Data[0] < (IR_PROTOCOL_SPACE - IR_PROTOCOL_TOLERANCE)))
        {
            _IR_MessageBuffer->Valid = false;
        }
        else
        {
            ...
        }
    }

    return _IR_MessageBuffer->Valid;
}

Dazu prüft die Funktion zuerst die Länge des Nachrichtenobjektes. Falls die Länge 2 beträgt, so handelt es sich um einen Repeat Code und das IsRepeat-Flag des Nachrichtenobjekts wird gesetzt. Anschließend wird der gespeicherte Wert für die Pausenzeit überprüft und wenn dieser Wert innerhalb der Toleranz liegt (± 5 Ticks), wird die empfangene Nachricht als gültig markiert, indem das Valid auf true gesetzt wird.

Falls es sich nicht um einen Repeat Code handelt, so werden die gespeicherten Zeitinformationen ausgewertet und in Bitinformationen umgewandelt. Anschließend werden die empfangenen Daten noch auf Fehler überprüft, indem der nicht-invertierte und der invertierte Wert miteinander verglichen werden.

for(uint8_t i = 0x01; i < (_IR_MessageBuffer->Length - 0x01); i += 0x02)
{
    _IR_MessageBuffer->Data.Value <<= 0x01;
    if((_IR_Data[i] < (IR_PROTOCOL_BIT_BURST + IR_PROTOCOL_TOLERANCE)) && (_IR_Data[i] > (IR_PROTOCOL_BIT_BURST - IR_PROTOCOL_TOLERANCE)) &&
       (_IR_Data[i + 0x01] < (IR_PROTOCOL_BIT_ZERO + IR_PROTOCOL_TOLERANCE)) && (_IR_Data[i + 0x01] > (IR_PROTOCOL_BIT_ZERO - IR_PROTOCOL_TOLERANCE))
    )
    {
        _IR_MessageBuffer->Data.Value |= 0x00;
    }
    else if((_IR_Data[i] < (IR_PROTOCOL_BIT_BURST + IR_PROTOCOL_TOLERANCE)) && (_IR_Data[i] > (IR_PROTOCOL_BIT_BURST - IR_PROTOCOL_TOLERANCE)) &&
        (_IR_Data[i + 0x01] < (IR_PROTOCOL_BIT_ONE + IR_PROTOCOL_TOLERANCE)) && (_IR_Data[i + 0x01] > (IR_PROTOCOL_BIT_ONE - IR_PROTOCOL_TOLERANCE))
    )
    {
        _IR_MessageBuffer->Data.Value |= 0x01;
    }
}
if((_IR_MessageBuffer->Data.Field[0x00] == (~_IR_MessageBuffer->Data.Field[0x01])) &&
   (_IR_MessageBuffer->Data.Field[0x02] == (~_IR_MessageBuffer->Data.Field[0x03])))
{
    _IR_MessageBuffer->Valid = false;
}
else
{
    _IR_MessageBuffer->Valid = true;
}

Die Auswertung der Daten ist damit komplett und kann nun getestet werden. Dazu habe ich ein kleines Beispielprogramm geschrieben, welches die Daten von einer Fernbedienung auswertet und die Hintergrundbeleuchtung des Displays ein- und ausschaltet:

#include "Interfaces/IR-Remote/NEC_IR.h"
#include "Services/DisplayManager/DisplayManager.h"

char DisplayBuffer[32];
IR_Message_t Message;

int main(void)
{
    SysClock_Init();
    IR_Init();
    DisplayManager_Init();
    DisplayManager_Clear();
    DisplayManager_DrawString(20, 0, "IR remote example");
    while(1)
    {
        if(IR_GetMessage(&Message))
        {
            DisplayManager_ClearLine(2);
            if(!Message.IsRepeat)
            {
                DisplayManager_DrawString(0, 8, "Button pressed!");
                switch(Message.Data.Value)
                {
                    case IR_REMOTE_KEY_1:
                    {
                        DisplayManager_SwitchBacklight(true);

                        break;
                    }
                    case IR_REMOTE_KEY_9:
                    {
                        DisplayManager_SwitchBacklight(false);

                        break;
                    }
                    default:
                    {
                        break;
                    }
                }
                sprintf(DisplayBuffer, "Data: 0x%lX", Message.Data.Value);
                DisplayManager_DrawString(0, 16, DisplayBuffer);
            }
            else
            {
                DisplayManager_DrawString(0, 16, "Repeat");
            }
        }
    }
    return 0;
}

Das komplette Beispiel könnt ihr euch in meinem GitHub-Repository herunterladen. Ggf. müssen die hinterlegten Codes noch der verwendeten Fernbedienung angepasst werden.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert