RC5-Decoder Software

Mit der Fernbedienung vom Wühltisch die eigenen Schaltungen steuern.

Mit einer Universal-Fernbedienung vom Wühltisch und einem Infrarot-Empfänger lässt sich leicht, preiwert und komfortabel eine Eingabeeinheit für einen µC aufbauen. IR-Empfänger-Bausteine wie TSOP1736, TSOP1836, SFH506-36, etc sind für weniger als 1€ erhältlich. Das einzige Problem ist dann, die Fernbedienung so einzustellen, daß sie RC5-Code sendet. Meist haben die Fernbedienungen nur ein Liste mit Gerätemarken anbei, in der steht, welchen Gerätecode man eingeben muss. Um die Bedienung RC5 senden zu lassen, muss man also erst etwas rumprobieren...

Hier der Software-Teil des Empfängers; bestehend aus drei C-Quellen:

Ein Header, der in den C-Modulen includet wird, wo man die RC5-Funktionalität braucht.
Einer Quelldatei, die Controller-unabhängigen C-Code enthält.
Einer Quelldatei, die Controller-abhängigen C-Code enthält, hier für avr-gcc und ein paar AVRs.

Zu compileren ist nur die µC-abhängige Quelle, welche den µC-unabhängigen Teil benutzt (includet). Die Quellen sind ausführlich dokumentiert – allerdings in Englisch.

Code-Statistik: 731 Zeilen (127 Zeilen ohne Leerzeilen und Kommentare).


µC-unabhängiger Teil (Header)

 * RC5 protocol implementation.
 * Contributed by Georg-Johann Lay <georgjohann@web.de>
 * @Author  : Georg-Johann Lay
 * @Date    : 2007-02-02
 * @File    : rc5.h
 * @Language: ISO C 99
 * @Purpose : Implementation of RC5 receiver software

  This implementation assumes a ONE to be encoded as HIGH-LOW transition
  and a ZERO encoded as LOW-HIGH transition, i.e. just the other way
  round as specified by the RC5 contract.
  This inversion comes from the IR receiver's open collector output driver
  that inverts the signal.

  The RC5 signal consists of 14 bits

  AGC (Automatic Gain Control)
      These two bits allow the RC5 receiver hardware's input amplifier to adapt
      to the signal level.

  TOGGLE bit (aka FLIP bit)
      This bit toggles each time a new key is pressed. That allows to
      distinguish between holding down a key and pressing a key twice or more.

      Five address bits to select a device (0=TV, 20=CD, ...).

      Six bits of very information. To support more than 64 commands, newer
      RC5 like protocols use a seventh bit whose complement is transmitted
      as second AGC bit.

  Thus, an RC5 frame looks like this:

  G ~C6  F  A4 A3 A2 A1 A0  C5 C4 C3 C2 C1 C0

  With C6 always 0 (i.e. second AGC always 1) if just 64 commands are supported.

  An RC5 bit

  consists of two parts: The first half bit and the second half bit which are
  separated by the respective signal level transition. Between two bits
  there may or may not be a transition.
  A Bit lasts 1728 µs.

#ifndef _RC5_H_
#define _RC5_H_

#include <inttypes.h>

  A typical snippet of C code will look like this:
  (Sample code for AVR and avr-gcc.
   Compile `rc5-avr.c' and link against the generated object.

      avr-gcc -Os -c rc5-avr.c -mmcu=... -DF_CPU=...)

  #include <avr/io.h>
  #include <avr/interrupt.h>

  #include "rc5.h"

  int main()
      // Initialize Hardware
      // Allow any RC5 device address
      rc5_init (RC5_ALL);

      // enable all interrupts

      // main loop
      while (1)
          // New RC5 frame arrived?
          if (rc5.flip >= 0)
              // Yes:
              // evaluate RC5 data in `rc5' or make a local copy
              // of it for later use

              // Tell receiver software to get more RC5 frames
              rc5.flip = -1;
          } // done RC5
      } // main loop

      // We never come here, thus no return needed.
      // It would be dead, anyway...

  The structure that represents an RC5 frame.
  If rc5.flip = -1, then the receiver will listen for a new RC5 frame
  an pass it in `rc5' upon reception, provided the address matches.
  After reception of a frame, `rc5.flip' will be 0 or 1.
  After you got the RC5 data, just set rc5.flip to -1 again to indicate
  you read the data and the next RC5 frame may be received.
typedef struct
    // The RC5 code (command)
    uint8_t code;

    // The device address: 0=TV ...
    uint8_t addr;

    // The flip bit
    volatile char flip;

    // This is not used by the RC5 software. It may be used by the application.
    // The main purpose of this field is to make `rc5_t' to have a size
    // of 4 bytes (instead of 3). This is much better if the user makes a local
    // copy of the rc5 object because the copy then can be hold in registers
    // and saves the overhead of a frame.
    char info;
} rc5_t;

// Publish `rc5'.
extern rc5_t rc5;

  Initialize the RC5 receiver software.
  Pass the device address to listen to in `addr'.
  If the MSB of `addr' is set, then all device addresses are accepted.
  You may use `RC5_ALL' as address in that case.
extern void rc5_init (uint8_t addr);

#define RC5_ALL 0xff

// There is an extension of the RC5 protocol that features a seventh
// command bit and thereby extends the number of commands from 64 to 128.
// This bit's complement is shipped as the second AGC bit. To activate the
// recognition of such RC5 frames (i.e. frames with command = 64...127)
// just define the following macro or add `-DRC5_RC6' to the compiler's
// command line arguments.

// #define RC5_RC6

#endif /* _RC5_H_ */


µC-unabhängiger Teil. Diese Datei wird nicht compiliert sondern vom hardware-abhängigen Teil includiert. Erklärung dazu findet sich im Quellkommentar.

 * RC5 decoder
 * Contributed by Georg-Johann Lay <georgjohann@web.de>
 * @Author  : Georg-Johann Lay
 * @Date    : 2007-02-02
 * @File    : rc5.c
 * @Language: ISO C 99
 * @Purpose : Implementation of RC5 receiver, target independent part.
 *            Do not compile this file.
 *            It is just included by the target dependent part `rc5-foo.h'.

  To implement an RC5 receiver there are basically two approaches:

  Look at the signal level in time gaps of a bit's or half bit's
  duration. This gives a straight forward and simple implementation.
  However, the time intervals must be followed very strictly.
  Otherwise, synchronization will get lost and the receiver won't
  work. So one needs a quite exact timer. Moreover, not all RC5
  transmitters follow exactly the RC5 signal standard and introduce
  tolerances in bit durations that make this approach obsolete.

  Synchronize with the signal at each signal edge and evaluate
  the time that elapsed since the last edge. This approch is much
  more powerful: It does not need sophisticated timer capabilities,
  allows both integrity checks of the received data and allowing
  tolerances in the transmitted protocol.

  We take way -2-

// The rc5 data struct to communicate with the application
// typedef'ed in `rc5.h'
rc5_t rc5;

// Some tweaks...
#ifdef __GNUC__
#   define inline __attribute__((always_inline))
#endif // GCC

// Target dependent stuff that is needed in `rc5_init'.
// The implementation can be found in the target dependent source file.
static inline void rc5_timer_run();
static inline void rc5_timer_enable_irq();
static inline void rc5_xint_enable();

// (Mostly) target independent RC5 algorithms, implemented in this file
static inline void rc5_timer_isr();
static inline void rc5_xint_isr();


// An RC5 frame has 14 bits:
// G1 G2[~C.6] F A4 A3 A2 A1 A0 C5 C4 C3 C2 C1 C0
// G: automatic Gain control bits
// F: flip bit, aka. toggle bit
// A: address
// C: command, complement if G2 may serve as seventh command bit
#define RC5_BITS_PER_FRAME 14

// Time in µs for a whole bit of RC5 (first & second half bit)
#define RC5_BIT_US   (64*27)

// Number of timer ticks for the duration of one RC5 bit.
// This can be used in a C source or by the preprocessor for
// a range check, i.e. a valid F_CPU/RC5_PRESCALE ratio.
#define RC5_TICKS_VAL \
    F_CPU / 1000 * RC5_BIT_US / 1000 / RC5_PRESCALE

#define RC5_TICKS \
    ((uint8_t) ((uint32_t) (RC5_TICKS_VAL)))

#define RC5_DELTA \
    ((RC5_TICKS_VAL) / 6)

// Test RC5_TICKS_VAL for a reasonable value
#if (RC5_TICKS_VAL > 200) || (RC5_TICKS_VAL < 12)
#error This is a bad combination of F_CPU and RC5_PRESCALE!

// This union allows to access 16 bits as word or as two bytes.
// This approach is (probably) more efficient than shifting.
union data16
    uint16_t asWord;
    uint8_t  asByte[2];

// Local information that we need just in this file
    // The very RC5 data buffer
    union data16 data;
    // The address we shall listen to:
    // If the MSB is set all addresses are welcome
    uint8_t      addr;
    // Number of edges (external interrupts) occured so far
    uint8_t      nedge;
    // Number of half bits bits received so far
    uint8_t      hbits;
} rc5_info;


void rc5_init (uint8_t addr)
    // Save RC5 address that we listen to
    rc5_info.addr = addr;

    // Reset half bit count
    rc5_info.hbits = 0;

    // .flip = -1: no RC5 data available, listen
    rc5.flip = -1;

    // Initialize the hardware


// This routine evaluates the RC5 frame and stores it in the
// `rc5' struct, provided it is ok and the address matches.
void rc5_timer_isr()
    // The collected bits.
    union data16 data = rc5_info.data;

    // Number of bits
    uint8_t nbits = (uint8_t) (1+rc5_info.hbits) / 2;

    // An RC5 frame consists of 14 bits.
    if (RC5_BITS_PER_FRAME == nbits
#ifdef RC5_RC6
        // check for the first AGC bit to be 1
        && data.asByte[1] >= 0x20
        // check for the two AGC bits to be 1
        && data.asByte[1] >= 0x30
#endif // RC6
        // Did the application allow reception of new RC5 data?
        && rc5.flip < 0)
        uint8_t rc5_code;
        uint8_t rc5_addr;

        // We do the bit manipulation stuff by hand because of code size.
        rc5_code = data.asByte[0] & 0x3f; // 0b00111111 : Bits #0..#5
#ifdef RC5_RC6
        // Second AGC is abused as ~code.6
        if (0 == (data.asByte[1] & 0x10))
            rc5_code |= (1 << 6);
#endif // RC6

        // Gather all address bits in data.asByte[1]
        data.asWord <<= 2;
        rc5_addr = data.asByte[1] & 0x1f; // 0b00011111 : Bits #6..#10

        // Are we addressee?
        if (rc5_info.addr == rc5_addr
            || rc5_info.addr >= 128)
            // Yes. Store the RC5 frame: code (command), address and flip.
            rc5.addr = rc5_addr;
            rc5.code = rc5_code;

            int8_t flip = 0;
            if (data.asByte[1] & 0x20) // 0b00100000 : Bit #11
                flip = 1;
            rc5.flip = flip;
    // If we land here and `hbits' is not as expected,
    // we did not store anything and do just a reset.

    // Reset and wait for the next RC5 frame
    rc5_info.hbits = 0;


// This routine collects the bits
void rc5_xint_isr()
    // The number of half bits.
    uint8_t hbits = rc5_info.hbits;

    // Either we are done with this frame or this is no RC5.
    // ...maybe some other protocol, maybe a transient
    // error or the like. Just run into the timer overflow and clean up
    // the mess there. Disabling XINT is not the best idea if we have
    // code size in mind. Errors will not occur very often, anyway.
    if (hbits >= 2*RC5_BITS_PER_FRAME -1)

    // Read the timer value.
    uint8_t timer = RC5_TIMER_REG;

    // Reset the timer to 0.
    RC5_TIMER_REG = 0;

    // The number of edges (interrupts) occured so far.
    uint8_t nedge = rc5_info.nedge;

    // Start of a new RC5 frame?
    if (0 == hbits)
        // Yes:
        // We are only interested in falling edges
        if (1 == (RC5_PIN_REG >> RC5_PAD) % 2)

        // Set all bits of RC5 frame and edge count to 0
        rc5_info.data.asWord = 0;
        nedge = 0;

        // Mimic the first AGC's first half bit
        timer = RC5_TICKS/2;

    // Number of half bits of the just elapsed period
    // is 1 or 2 or invalid.
    // As RC5 toggles in the middle of each bit one interval of
    // constant signal level lasts RC5_TICKS or RC5_TICKS/2.
    // In the first case, a bit's second part has the same level
    // as the subsequent bit's first part.

    // We received at least one half bit.

    // Test interval's length
    if (timer > RC5_TICKS - RC5_DELTA)
        // Two half bits: one more...

        // reduce the validity check to the case of one half bit.
        timer -= RC5_TICKS/2;

    // Test if a half bit's length is invalid:
    // |timer - RC5_TICKS/2| > RC5_DELTA ?
    if (timer < RC5_TICKS/2 - RC5_DELTA
        || timer > RC5_TICKS/2 + RC5_DELTA)
        // Invalid:
        // Setting `nbits' to some invalid value will make
        // this ISR and the timer ISR ignore the rubbish.
        hbits = 0xff;

        // Force a soon timer overflow
        RC5_TIMER_REG = -RC5_TICKS;
        // Valid:
        // Test if the edge occured in the middle of a bit, i.e.
        // if the number of half bits is odd.
        // If so, this edge tells us the bit's value.
        if (hbits % 2 == 1)
            // We shift data left by one and then store the
            // received bit in the LSB. This is best because
            // RC5 is big endian (MSB first).
            union data16 data = rc5_info.data;
            data.asWord <<= 1;

            // Store the bit. Edge polarity toggles; due to signal
            // inversion, a falling edge (even) is a ONE and
            // a raising edge (odd) is a ZERO.
            // Note that edge count starts at 0.
            if (nedge % 2 == 0)
                data.asByte[0] |= 1;

            // Store data
            rc5_info.data = data;

            if (hbits >= 2*RC5_BITS_PER_FRAME -1)
                // We have collected all bits.
                // Maybe there will be one more IRQ (hbit) but we do not care.
                // Frame is complete: force a soon timer overflow.
                RC5_TIMER_REG = -RC5_TICKS;

    // write back the local copies
    rc5_info.hbits = hbits;
    rc5_info.nedge = 1+nedge;


µC-abhängiger Teil (AT90S2313, ATtiny2313, ATmega8/16/32/48/88/168).

 * RC5 implementation for some AVR derivatives.
 * Contributed by Georg-Johann Lay <georgjohann@web.de>
 * @Author  : Georg-Johann Lay
 * @Date    : 2007-02-02
 * @File    : rc5-avr.c
 * @Language: ISO C 99
 * @Purpose : Implementation of RC5 receiver, target dependent part
 *            for some AVR derivatives.

  For the best results, use optimization '-Os' (optimize for size).

  On one hand, we want the implementation to be as efficient as
  possible because we run on a hardware with quite limited resources (AVR).
  On the other hand, we want the code to be as portable as can be.
  Therefore, we follow an approach that looks a bit odd at first sight:
  We include a C source.
  (You may rename it to `foo.h' and feel more familiar.)

  This has two reasons: First, we keep the target independent part of
  the implementation separated from the target dependent part.
  Second, including the C code (in contrast to linking against an
  object or a library) supplies the compiler with a great amount
  of additional information. In particular, it is supplied with the
  very source code. That information, in turn, allows optimization
  strategies like inlining, better register allocation, etc.

  For this reason, the target dependent part and the target independent
  part are intertwined and the abstraction from the hardware is just on
  the source level, not on the object level. (This was our goal with
  respect to optimization!). However, this code may serve as a starting
  point if you want to implement a hardware abstraction by means of
  objects, i.e. supply it in a library.

  On AVR, the code eats up at about 320 bytes of your valuable flash and
  needs at about 20 bytes of SRAM total (static and dynamic).
  The incraese of interrupt respond times can be analysed statically,
  but I did not investigate in that.

  avr-gcc does not support all AVR derivatives on C level. Smaller AVRs
  are just supported on assembler level. To get a start for an assembler
  implementation, compile the module for an AVR that is "close" to
  the AVR you like and then look at avr-gcc's assembler output.

  - Peek at avr-gcc's assembler output
      > avr-gcc -S rc5-avr.c -fverbose-asm -Os -mmcu=... -DF_CPU=...
      > avr-gcc -c rc5-avr.c -fverbose-asm -Os -mmcu=... -DF_CPU=... -save-temps

  - Compile with debug info and disassemble the object (elf32-avr):
      > avr-gcc -g -c rc5-avr.c -Os -mmcu=... -DF_CPU=...
      > avr-objdump -h -S -j .text rc5-avr.o
    Disassembling an Intel HEX file or the like is not recommended because
    any debug, symbol or source information will be lost.

  Again, the ISR implementation will be mostly hardware independent.
  The hardware dependent stuff accumulates in `rc5_init', so you best
  write that routine by hand. Concerning the ISRs, there is some potential
  for optimization; particularly in the ISRs' prologue and epilogue.

// Test if we are avr-gcc
#if !defined (__GNUC__) || !defined (__AVR__)
#   error This code is hardware dependent and intended for avr-gcc!
#endif // avr-gcc

#include <avr/io.h>
#include <avr/interrupt.h>

// On older avr-gcc we need `avr/signal.h' (nowadays in `avr/interrupt.h').
#ifndef SIGNAL
#    include <avr/signal.h>

#include "rc5.h"

// The timer's counter register, counting up. As Timer0 comes along
// with the least resources, we use that timer. It is an 8 bit timer
// that overflows at 255 to 0 and generates a timer overflow IRQ each
// time it does so.
// If you use a 16 bit timer, make sure it has the same behaviour, e.g.
// by using its "clear timer on compare match" functionality.

// The hardware port the IR receiver is connected to: INT0
// You may also use INT1 or use PCINT and filter for events that come
// from the IR receiver. You may also configure the analog comparator
// to work as external events' interrupt generator.
#define RC5_PIN_REG   PIND
#define RC5_PAD       PORTD2

// We need the frequency the controller is running at.
// `F_CPU' is a commonly used identifier for it.
#ifndef F_CPU
#   error Please define F_CPU by adding a source line like #define F_CPU ...
#   error or by specifying the -D compiler option like '-DF_CPU=...'
#endif // F_CPU

  Set `RC5_PRESCALE' to some reasonable default. It is the prescaler
  to use with the RC5 timer. As you can imagine, I did not test it
  for every possible F_CPU, so I hope for the best...
  Newer AVR derivatives provide a more fine grained prescaler.

  Allow `RC5_PRESCALE' to be defined on the command line with "-DRC5_..."
#ifndef RC5_PRESCALE
#   if F_CPU >= 12000000
#       define RC5_PRESCALE 1024
#   elif F_CPU >= 4000000
#       define RC5_PRESCALE 256
#   else
#       define RC5_PRESCALE 64
#  endif // switch F_CPU
#endif // RC5_PRESCALE

// The implementation of target independent RC5 stuff
#include "rc5.c"

// The interrupt service routines (ISRs)

// I implemented the two ISRs as SIGNAL, i.e. they cannot be interrupted
// because the I-flag is unset by the AVR hardware during their execution.
// If you want to use the code in an application that has to fit harder
// real time requirements, i.e. shorter interrupt respond times for
// other IRQs, then you can implement the ISRs as INTERRUPTs or doing
// things like yield().
// However, there are some caveats. One of them is that you must care
// for and handle frequent transient errors because otherwise
// `rc5_xint_isr' will begin to recurse. One approach could be to
// disable only the external interrupt during execution of that routine.
// An other approath could be to lock the routine...

// The timer overflow interrupt service routine

// The external interrupt's ISR
// This might also be a PCINT, a pin change interrupt on newer AVRs.
// If you use multiple pin change interrupts, then you have to add a test
// wether or not the pin change event comes from the IR receiver's pin.
// You can also configure the Analog Comparator to work as a
// source of pin change events.

  Here we go. What we have to implement:

  void rc5_timer_run()         Run (start) the overflow timer
  void rc5_timer_enable_irq()  Enable timer overflow IRQs
  void rc5_xint_enable()       Enable edge triggered external interrupt IRQs

  `RC5_TIMER_RUN' is the value that must be written to the
  timer configuration register in order to start it.

  The values below fit the controllers below.

  For other AVR derivatives this may look completely different.
  If so, take care for it in your own derivative's section below.
#if (RC5_PRESCALE==1024)
#   define      RC5_TIMER_RUN ((1 << CS02) | (1 << CS00))
#elif   (RC5_PRESCALE==256)
#   define      RC5_TIMER_RUN (1 << CS02)
#elif   (RC5_PRESCALE==64)
#   define      RC5_TIMER_RUN ((1 << CS01) | (1 << CS00))
#   error This RC5_PRESCALE is not supported
#endif // RC5_PRESCALE

#if defined (__AVR_ATmega8__) || defined (__AVR_AT90S2313__) \
    || defined (__AVR_ATmega32__) || defined (__AVR_ATmega16__)

void rc5_timer_run()

void rc5_timer_enable_irq()
    TIMSK |= (1 << TOIE0);

void rc5_xint_enable()
    MCUCR = (MCUCR | (1 << ISC00)) & ~ (1 << ISC01);

#if defined (__AVR_AT90S2313__)
    GIMSK |= (1 << INT0);
    GICR |= (1 << INT0);
#endif // AT90S2313

//  MCUCR = (MCUCR | (1 << ISC10)) & ~ (1 << ISC11);
//  GIMSK |= (1 << INT1); // S2313
//  GICR |= (1 << INT1);  // M8

#elif defined (__AVR_ATtiny2313__)

void rc5_timer_run()

void rc5_timer_enable_irq()
    TIMSK |= (1 << TOIE0);

void rc5_xint_enable()
    // INTx on both edges
    MCUCR = (MCUCR | (1 << ISC00)) & ~ (1 << ISC01);
    GIMSK |= (1 << INT0);

//  MCUCR = (MCUCR | (1 << ISC10)) & ~ (1 << ISC11);
//  GIMSK |= (1 << INT1);

#elif defined (__AVR_ATmega48__) || defined (__AVR_ATmega88__) \
    || defined (__AVR_ATmega168__)

void rc5_timer_run()

void rc5_timer_enable_irq()
    TIMSK0 |= (1 << TOIE0);

void rc5_xint_enable()
    // INTx on both edges
    EICRA = (EICRA | (1 << ISC00)) & ~ (1 << ISC01);
    EIMSK |= (1 << INT0);

//  EICRA = (EICRA | (1 << ISC10)) & ~ (1 << ISC11);
//  EIMSK |= (1 << INT1);

#   error This AVR derivative is (not yet) supported.
#   error Feel free to add the required code.
#endif // AVR derivatives