Select Page

Difficulty Level = 5 [What’s this?]

UPDATE: Also see this project for an easy way to display a temperature reading: Digit Shield Temperature Display.


A while back, I did a wireless temperature sensor project using XBee radios. XBee radios are really powerful devices with good reliability and the ability to read and transmit sensor readings without a microcontroller. BUT, they are difficult for people to configure, the documentation is hard to understand, and it’s really difficult to parse the API data packets at the receiving station. So I decided to try the same project using inexpensive RF devices. I used a 434MHz transmitter ($4) and receiver ($5) from Sparkfun, and had great success with these cheap devices. I also used a simple LM34 Fahrenheit temperature sensor, two Arduinos (one was a homemade breadboard version), and a two-digit LED display to show the temperature at the receiver end.

The Receiver

RF receiver with temperature display

I used a single 74LS247 BCD to 7-segment driver chip for the display. The segment pins of the two digits are connected together, so I need to multiplex between the digits to show only one at a time. The multiplexing is so fast, there’s no flicker. The segments have common anodes and I used two PNP transistors to provide current to the anodes. Don’t ever drive the anodes directly from an Arduino output pin because it’s too much current!

The RF receiver’s data pin is connected to the RX pin on the Arduino so we can just use the Serial library to read data at a slow 1200 bps. That is fast enough for temperature sensor readings. A 17cm antenna (the green wire) is attached to the ANT pin on the RF receiver.

RF Receiver Schematic (click to enlarge)

Here is the code running on the receiver Arduino. These RF devices can pick up a lot of noise, so drawing on the work of others, I used a simple protocol which includes a packet header with a network identifier and target address, the data, and a checksum to catch errors. This provides robustness for the communication link.

Another important note is that the multiplexing between the two display digits is handled in an interrupt handler. The loop() code never has to do anything with the display. Timer2 is configured to fire an interrupt when the timer2 counter overflows, so the ISR near the end of this code runs about every 2ms and simply toggles which display is active. This is fast enough to provide perfectly smooth display without any fuss. All the register manipulation in the setup() method is the configuration of Timer2. I really like this way of handling the multiplexing automatically. You can download the code here.

#define PACKET_HEADER_SIZE 3
#define PAYLOAD_SIZE 1
#define CHECKSUM_SIZE 1
#define PACKET_SIZE (PACKET_HEADER_SIZE + PAYLOAD_SIZE + CHECKSUM_SIZE)
#define ADDR 1

byte temp = 0;
byte d1; // left digit
byte d2; // right digit
byte digitToggle = 0;

const byte packetHeader[PACKET_HEADER_SIZE] = {0x8F, 0xAA, ADDR};

void setup()
{
  Serial.begin(1200);

  for(int i=2;i<=7;i++) {
    pinMode(i, OUTPUT);
  }
  digitalWrite(6, HIGH);
  digitalWrite(7, HIGH);

  // Disable the timer overflow interrupt
  TIMSK2 &= ~(1 << TOIE2);

  // Set timer2 to normal mode
  TCCR2A &= ~((1 << WGM21) | (1 << WGM20));
  TCCR2B &= ~(1 << WGM22);

  // Use internal I/O clock
  ASSR &= ~(1 << AS2);

  // Disable compare match interrupt
  TIMSK2 &= ~(1 << OCIE2A);

  // Prescalar is clock divided by 128
  TCCR2B |= (1 << CS22)  | (1 << CS20);
  TCCR2B &= ~(1 << CS21);

  // Start the counting at 0
  TCNT2 = 0;

  // Enable the timer2 overflow interrupt
  TIMSK2 |= (1 << TOIE2);
}

void loop() {
  temp = readByte();
  setValue(temp);
}

byte readByte() {
  int pos = 0;
  byte val;
  byte c = 0;

  while (pos < PACKET_HEADER_SIZE) {
    while (Serial.available() == 0); // Wait until something is available
    c = Serial.read();

    if (c == packetHeader[pos]) {
      if (pos == PACKET_HEADER_SIZE-1) {
        byte checksum;

        // Wait until something is available
        while (Serial.available() < PAYLOAD_SIZE + CHECKSUM_SIZE); 
        val =  Serial.read();
        checksum =  Serial.read();

        if (checksum != (packetHeader[0] ^ packetHeader[1] ^ packetHeader[2] ^ val)) {
          // Checksum failed
          pos = -1;
        }
      }
      pos++;
    } else {
      if (c == packetHeader[0]) {
        pos = 1;
      } else {
        pos = 0;
      }
    }
  }
  return val;
}

void setValue(byte n) {
  d1 = n / 10;
  d2 = n % 10;
}

void setOutput(byte d) {
  // This is more complex because the 74LS247 inputs are on
  // nonconsecutive Arduino output pins (for ease of soldering).
  PORTD &= ~0x3C; // turn off digital pins 2-5
  if ((d & 0x1) > 0) {
    // 74LS247 input A connected to Arduino pin 5
    PORTD |= (1 << PORTD5);
  }
  if ((d & 0x2) > 0) {
    // 74LS247 input B connected to Arduino pin 2
    PORTD |= (1 << PORTD2);
  }
  if ((d & 0x4) > 0) {
    // 74LS247 input C connected to Arduino pin 3
    PORTD |= (1 << PORTD3);
  }
  if ((d & 0x8) > 0) {
    // 74LS247 input D connected to Arduino pin 4
    PORTD |= (1 << PORTD4);
  }
}

// Interrupt service routine is invoked when timer2 overflows.
ISR(TIMER2_OVF_vect) {
  TCNT2 = 0;
  if (digitToggle == 0) {
    PORTD |= (1 << PORTD7);  // turn off digit 2
    setOutput(d1);
    PORTD &= ~(1 << PORTD6); // turn on digit 1
  } else {
    PORTD |= (1 << PORTD6);  // turn off digit 1
    setOutput(d2);
    PORTD &= ~(1 << PORTD7); // turn on digit 2
  }
  digitToggle = ~digitToggle;
}

The Transmitter

RF transmitter and temperature sensor on breadboard Arduino

The unfortunate thing about using the RF transmitter is that it's a lot dumber than an XBee radio (but a lot cheaper, too). It has no ability to read the voltage on a pin and transmit it, so I had to use a microcontroller of some sort. At first I wanted to use an ATtiny13, but it has no UART for the serial transmission control. Then I wanted to use my ATtiny2313 which does have a UART, but doesn't have an analog to digital converter (ADC)! The smallest AVR microcontroller that I own that has both ADC and UART is the ATmega328. At this point I figured I might as well use a full-blown Arduino. I don't have two Arduinos so I built a breadboard version for use at the transmitter. You can just use a regular Arduino if you have one handy, or team up with a friend who has one.

An LM34 temperature sensor outputs a variable voltage depending on the temperature. The mapping is extremely simple: 10mV for every Fahrenheit degree. So, at 72 degrees F, the output is 720mV. The Arduino TX pin is simply connected to the data pin on the transmitter.

Here's the schematic. There's an LED attached to pin 13 that I blink every time a sensor reading is transmitted (not shown in schematic).

RF Transmitter Schematic (click to enlarge)

Here's the code running on the transmitter ATmega328. You can download the code here.

#define SENSOR_ANALOG_PIN 0
#define LED_PIN 13
#define PACKET_HEADER_SIZE 3

#define ADDR 1

const byte packetHeader[PACKET_HEADER_SIZE] = {0x8F, 0xAA, ADDR};

byte temp;

void setup() {
  Serial.begin(1200);
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_PIN, HIGH);
  readTemp();
  writeByte(temp);
  digitalWrite(LED_PIN, LOW);
  delay(1000);
}

void readTemp() {
  byte reading = analogRead(SENSOR_ANALOG_PIN);
  int mv = ((float)(reading/1023.0)) * 5000;
  temp = mv / 10;
}

// Sends an unsigned int over the RF network                                                            
void writeByte(byte val) {
  byte checksum = (packetHeader[0] ^ packetHeader[1] ^ packetHeader[2] ^ val);
  Serial.write(packetHeader, PACKET_HEADER_SIZE);
  Serial.write(val);
  Serial.write(checksum);
}

That's all there is to it! This setup allowed me to transmit sensor readings reliably throughout my home, especially with a 17cm (1/4 wavelength of 434Mhz signal) antenna at the transmitter and receiver.