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
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 received | Interpreted as |
---|---|
000 | 0 |
001 | 0 |
010 | 0 |
100 | 0 |
111 | 1 |
110 | 1 |
101 | 1 |
011 | 1 |
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 Position | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
Content | 0xAA | 5 | 0x00 | 1 | 110 | 0xD6 | 0xFE |
Description | Magic byte | Package len | Payload | Sender ID | Package ID | CRC16 |
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
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
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
- VirtualWire library - http://www.airspayce.com/mikem/arduino/VirtualWire/
- RadioHead library - http://www.airspayce.com/mikem/arduino/RadioHead/
- rc-switch library - https://github.com/sui77/rc-switch
- RFTransmitter library - https://github.com/zeitgeist87/RFTransmitter
- RFReceiver library - https://github.com/zeitgeist87/RFReceiver
- Wikipedia on Differential Manchester Encoding - https://en.wikipedia.org/wiki/Differential_Manchester_encoding
- Wikipedia on Forward Error Correction - https://en.wikipedia.org/wiki/Forward_error_correction
- Wikipedia on Repetition Code - https://en.wikipedia.org/wiki/Repetition_code
- Wikipedia on Majority Logic Decoding - https://en.wikipedia.org/wiki/Majority_logic_decoding