My Notebook

New Arduino library for 433 MHz AM radio modules

Author
Date
Category
Electronics
433 MHz AM module

I have just finished my new Arduino library for 433 MHz AM radio modules with a focus on reliable one-way communication and forward error correction. There already are a few libraries that work with these cheap little modules, namely VirtualWire and its successor RadioHead as well as rc-switch, but I wasn't quite happy with either of them.

RadioHead for example is quite big and has an unnecessary dependency on the SPI library. VirtualWire is no longer maintained and it seems to be quite sensitive to radio interference. I couldn't get it to work reliably even at close range. Maybe that's because there is some kind of AM interference in my apartment. Finally rc-switch is optimized to be compatible with remote controlled light or mains switches and is not really suitable for the transmission of arbitrary data packets.

Source Code

The source code for this library is actually split into two libraries that work together. Both of them are published on GitHub (RFTransmitter and RFReceiver) under the Gnu GPL.

General Design

Bit Encoding

Bit-encoding waveform

The bit-encoding is currently very simple. Every bit takes 4 pulses. The pulse-length can be freely determined by the user. A binary 0 is encoded as one HIGH and three LOW pulses and a binary 1 as three HIGH and one LOW pulses. This is the same encoding that is used by the rc-switch library and I found in my experiments, that it achieved the best range. I will probably switch to a more efficient differential Manchester code in the future.

Forward Error Correction

A Forward Error Correction code adds redundancy to the message, which can be used by the receiver to correct for transmission errors and radio interference. It is widely used by almost all modern digital information transmission and storage systems. There are lots of very efficient mathematical FEC codes, but the simplest and most primitive form is what's known as a repetition code. The message is simply repeated a certain number of times, which allows the receiver to use a majority vote to recover the original bit.

The following table illustrates how a majority vote works for a triplet of three redundant bits:

Triplet receivedInterpreted as
0000
0010
0100
1000
1111
1101
1011
0111

My new Arduino library uses a slightly modified version of this simple repetition code, whereby every byte is sent three times. The reason for this is, that it is very simple to implement and the encoding and decoding is very fast on a microcontroller. The inefficient repetition code reduces the available data rate to a third, but I think the complex arithmetic needed for more efficient error correction codes would be too slow on most Arduinos.

Package Format

The package format is straight forward. The magic byte 0xAA indicates that a new package is about to start. The package length has to be between 4 and MAX_PACKAGE_SIZE, whereby MAX_PACKAGE_SIZE can go up to 255. The default value for MAX_PACKAGE_SIZE is 84 to reduce the size of the input buffer needed for the receiver.

Byte Position0123456
Content0xAA50x0011100xD60xFE
DescriptionMagic bytePackage lenPayloadSender IDPackage IDCRC16

After the payload follow the sender and package ids. The sender id, as the name suggests, is a unique id that identifies the transmitting device. The package id is used to recognize and ignore repeated transmissions of the same package.

Transmitter

The source code for the transmitter is in a separate library. This reduces the code size and memory footprint, if only the transmitter hardware module is used.

Constructor

The constructor has lots of parameters, but only the outputPin is required and everything else can be left on its default value. If you use more than one transmitter you should probably also set the nodeId to a unique value. It could also be necessary to tweak the pulseLength so that it works better with a particular hardware module.

RFTransmitter(byte outputPin, byte nodeId = 0,
    unsigned int pulseLength = 100, unsigned int backoffDelay = 100,
    byte resendCount = 3) :
    nodeId(nodeId), outputPin(outputPin), pulseLength(pulseLength),
    resendCount(resendCount), packageId(0), backoffDelay(backoffDelay) {

  pinMode(outputPin, OUTPUT);
  digitalWrite(outputPin, LOW);
}

The resendCount sets the number of times the transmission of a packet is repeated. The backoffDelay parameter sets the time in milliseconds between the repeated transmissions. The actual backoff-delay will be a random value between backoffDelay and backoffDelay * 2. If two transmitters interfere with each other, they will both wait for a different random time period and hopefully not interfere on the next transmission. This increases the likelihood, that a packet gets through random noise and interference from other transmitters at the cost of a lower data rate.

Connection Diagram

Connection diagramm for transmitter

Example

The following example shows the simplest possible transmitter. It sends the string "Hello World!" every 5 seconds. After the delay it resends the previous package again to make sure it is received and to compensate for any interference during the first transmission.

#include <RFTransmitter.h>

#define NODE_ID          1
#define OUTPUT_PIN       11

// Send on digital pin 11 and identify as node 1
RFTransmitter transmitter(OUTPUT_PIN, NODE_ID);

void setup() {}

void loop() {
  char *msg = "Hello World!";
  transmitter.send((byte *)msg, strlen(msg) + 1);

  delay(5000);
  
  transmitter.resend((byte *)msg, strlen(msg) + 1);
}

Receiver

The source code for the receiver is in a separate library.

Constructor

You only need to set the inputPin, which is connected to the data pin on the receiver module. The pulseLength needs to be the same on the receiver and transmitter side. Unfortunately it's currently not possible to recover the clock from the received signal alone, but that could be a future extension to the library.

RFReceiver(byte inputPin, unsigned int pulseLength = 100) :
    inputPin(inputPin), pulseLength(pulseLength), inputBufLen(0),
    inputBufReady(false), bitCount(0), byteCount(0),
    errorCorBufCount(0), lastTimestamp(0), packageStarted(false),
    shiftByte(0), lastSuccTimestamp(0) {

}

Synchronization

Every received bit gets right shifted into a buffer byte called shiftByte, whereby the newest bit enters from the left and the oldest bit gets dropped at the right. So the incoming bits flow through this byte sized buffer and if it at any point contains the magic byte 0xAA, a new package is started and the transmitter and receiver are synchronized.

All the following bytes are interpreted as part of the new package. Whenever the bitCount is equal to 8 the shiftByte is full and it is passed to the function decodeByte() for further processing.

shiftByte >>= missingBits;
if (!state)
  shiftByte |= 0x80;

if (packageStarted) {
  bitCount += missingBits;
  if (bitCount != 8)
    return;
  bitCount = 0;

  decodeByte(shiftByte);
} else if (shiftByte == 0xAA) {
  // ...
  // New package starts here
  packageStarted = true;
}

Error Correction

The checkBits macro takes three bytes and a bit position as input and returns the majority vote, described above. The recoverByte function applies this macro to every bit in a byte and ORs the result back together.

#define checkBits(b1, b2, b3, pos) (((((b1) & (1 << pos)) != 0) + \
                                   (((b2) & (1 << pos)) != 0) + \
                                   (((b3) & (1 << pos)) != 0)) > 1)

static inline byte recoverByte(const byte b1, const byte b2,
                               const byte b3) {
  byte res;

  if (b1 == b2 && b1 == b3)
    return b1;

  res = checkBits(b1, b2, b3, 0);
  res |= checkBits(b1, b2, b3, 1) << 1;
  res |= checkBits(b1, b2, b3, 2) << 2;
  res |= checkBits(b1, b2, b3, 3) << 3;
  res |= checkBits(b1, b2, b3, 4) << 4;
  res |= checkBits(b1, b2, b3, 5) << 5;
  res |= checkBits(b1, b2, b3, 6) << 6;
  res |= checkBits(b1, b2, b3, 7) << 7;

  return res;
}

This simple forward error correction code can recover single bit errors and enables a more reliable transmission for bigger packets. Unfortunately sending every byte three times also wastes a lot of bandwidth.

Connection Diagram

Connection diagramm for receiver

Example

#include <PinChangeInterruptHandler.h>
#include <RFReceiver.h>

// Listen on digital pin 2
RFReceiver receiver(2);

void setup() {
  Serial.begin(9600);
  receiver.begin();
}

void loop() {
  char msg[MAX_PACKAGE_SIZE];
  byte senderId = 0;
  byte packageId = 0;
  byte len = receiver.recvPackage((byte *)msg, &senderId, &packageId);

  Serial.println("");
  Serial.print("Package: ");
  Serial.println(packageId);
  Serial.print("Sender: ");
  Serial.println(senderId);
  Serial.print("Message: ");
  Serial.println(msg);
}

References