Solar Powered Arduino With XBee Radio

This site has been great fun, but it has run its course, and it is time to move on. Please get what you want from the site before January 1, 2020. Some pages will move to another site but most will go away.

Solar Powered Arduino with XBee 2.4GHz radio

XBee radios

XBee Zigbee XB24C7WIT is the radio I'm using on both ends. The first thing to do is flash them with new firmware, but these are considered "legacy" devices. XCTU, the XBee configuration software, had to download the legacy firmware, which is a huge zip file containing every version of firmware ever released. Once it had done that, I told the XCTU program to use XBee 802.15.4 firmware 2001 (the latest). Be sure you have the antennae on both radios oriented the same way. It makes a big difference in the reception. I had one horizontal and one vertical and it performed poorly.

There is a tutorial on the Digi website.

If you have a couple of XBee to USB converter boards, you can connect to both devices and run data through them in both directions. It is worth the invenstment in two USB to XBee boards to save time troubleshooting. The boards show you when the XBee turns its radio on and when serial data is being sent and received. Once you have the XBees working, you transfer one of them to your remote circuit and keep the other one in the USB adapter.

Power Requirements

The XBee has some pretty high specified power requirements at 33mA to 45mA transmit and 28mA to 31mA receive. The solar Arduino Pro Mini this is based on draws about 260µA without a µSD card, but the 3.3V regulator can source about 150mA, so no other regulation is needed. The whole circuit takes about 460µA until the Arduino drops DTR one second before transmitting, then it jumps up to 20mA, then when the data is sent, it stays at 20 mA for another 2.5 seconds. It drops again to 460µA and stays there until next time.

If we send something every minute, there will be 3.5 seconds of 20mA and 56.5 seconds of 460µA. That averages out to be 1.59mA. Not much. I'll say 2mA to keep the math easier. 2mA x 24 hours in a day is 48mAH usage per day.

I'll pretend we're in Marseille, France this time, at a latitude of 43°N, the day is 9.25 hours long on December 21. Marseille has 17 sunny days in December, so 17 ÷ 31 = 55% useful sun. 55% of 9.25 hours is 5.1 hours. That is how long we have to charge the batteries to make up for the use of 144mAH. That is 144 ÷ 5.1 = 28mA charging rate.

I should say now that all of this is wrong, but it averages out to correct. You can't design for the weather on any particular December 21st. You design for a partly cloudy (as defined locally) December 21st and let the tilt of the Earth's axis take care of the other days. They're all longer.

Solar Panel

We can do this with a single 12V 1.5W panel. The panel can generate a maximum of 84mA. Even though it may not be a clear day, and clouds come and go, the batteries will charge at a rate of (84mA - 2mA) = 82mA during sunny periods - not the designed for 28mA, which is simply the minimum required to charge in the available time. Having that extra capacity in the solar panel means it can charge the battery quickly when it has been cloudy for a couple of days.

These are the characteristic curves for the 12V 1.5W panel I used. It is spec'd at 1.5 watts, but it doesn't get there, producing only 0.92W peak. Still, it is enough for this task. It should charge the batteries fully in one day, on the 21st of December.

Charging Circuit

Yes. It is the same as the Solar 433MHz, Solar WiFi, and the Solar Arduino Uno. "If it ain't broke, don't fix it." It works and it's simple. The circuit is a voltage regulator with a transistor to shut it down. It charges as long as the transistor is off, and stops charging when the transistor is on. The Arduino determines when that happens based on the battery voltage.

It is set to 8.25V, or thereabouts, with the idea that it will always try to put some charge into the battery if it is enabled. If the battery is low, it will put a great deal of current into the battery, and if the battery is mostly charged, it will put only a little current into it. The Arduino turns the charger off when the battery voltage exceeds a high value and turns it back on when it is less than a low value. In that way the Arduino keeps the battery topped off during the day so it is ready for a long night with no sun.

The Measurement Circuit

The ADC reference was changed from the 5V supply to the internal 1.1V. I added three dividers, each having a 100kΩ resistor and a 10kΩ trimmer pot. The pot was adjusted to make the reading from the ADC match the reading on a digital meter.

The solar panel, charger, and battery voltages are sensed by the three resistor dividers. They cut the max voltage from 15V down to 1.1V, and feed it to the analog inputs A0, A1 and A2. Each ADU is worth 1.074mV, but we divided the battery, charger, and panel voltage by 13.6363 using the resistors, so each ADU is worth 14.645mV at the battery, charger, or solar panel. We need to know this for the code that reads the analog inputs and converts them to a voltage.

XBee Wiring

The wiring of the XBee module is pretty simple. Hook up the Arduino's transmit and receive as shown, add D5 to DTR to wake it up, and 3.3V and ground. You can add a 1µF ceramic or a 10µF tantalum between Vcc and ground to keep XBee noise off of the power line.

Firmware

The code is very similar to the others, but all serial prints have been stripped out, since they would go over the air to the receiver and corrupt the saved CSV file.

// The timer0 interrupt is disabled, so the delay()
// functions will not work. Use _delay_ms() instead.

#include <avr/power.h>
#include <avr/sleep.h>
#include <util/delay.h>
#include <LowPower.h>
#include "RTClib.h"

#define DS_3231
//#define DS_1307

#ifdef DS_3231
RTC_DS3231 rtc;
#else
RTC_DS1307 rtc;
#endif

#define V_PANEL A0
#define V_CHARGER A1
#define V_BATTERY A2
#define MIN_SOLAR (double)8.9
#define MAX_BATTERY (double)8.25
#define MIN_BATTERY (double)8.16

// Blips for one second every minute.
#define INDICATOR 3
#define INDICATOR_ON 1
#define INDICATOR_OFF 0

// Shuts down the charging circuit.
#define SHUTDOWN  4
#define SHUTDOWN_YES 1
#define SHUTDOWN_NO 0

#define SQUAREWAVE 2

// Pulse low to wake up XBee
#define SLEEP_PIN 5
#define SLEEP_YES 1
#define SLEEP_NO 0

// Reset XBE by lowering
#define XBEE_RESET 6
#define XBEE_RESET_YES 1
#define XBEE_RESET_NO 0


// This is the mV at the battery/panel for one ADU:
// (10v / 1.1v) * (1.1v / 1024)
// 13.636363 * 0.00107421875
#define ADC_FACTOR (double)0.0146484375

int ticker = 0;
char buf[40];
char tmpBuf[8];
double v_panel, v_charger, v_battery;
DateTime now;

/*****************************************************************************
 * isShutdown
 * 
 * Reports on the state of the SHUTDOWN bit.
 *****************************************************************************/

bool isShutdown() {
  return digitalRead(SHUTDOWN)?true:false;
}

/*****************************************************************************
 * getSolarVoltage
 * 
 * Returns the voltage on the solar panel.
 *****************************************************************************/

double getSolarVoltage() {
  return analogRead(V_PANEL) * ADC_FACTOR;
}

/*****************************************************************************
 * getChargerVoltage
 * 
 * Returns the voltage on the charger output.
 *****************************************************************************/

double getChargerVoltage() {
  return analogRead(V_CHARGER) * ADC_FACTOR;
}

/*****************************************************************************
 * getBatteryVoltage
 * 
 * Returns the voltage on the battery bus.
 *****************************************************************************/

double getBatteryVoltage() {
  return analogRead(V_BATTERY) * ADC_FACTOR;
}

/*****************************************************************************
 * saveReadings
 * 
 * Reads the voltages off the analog pins and writes
 * them as a line in a CSV file on the SD card.
 *****************************************************************************/

void saveReadings() {

  memset(buf, 0, sizeof(buf));
  sprintf(buf, "%4d-%02d-%02d %02d:%02d,",
    now.year(),
    now.month(),
    now.day(),
    now.hour(),
    now.minute()
  );
  // Read the panel voltage.
  v_panel = getSolarVoltage();
  
  // Read the battery voltage.
  v_battery = getBatteryVoltage();

  // Read the charger voltage.
  v_charger = getChargerVoltage();
  
  // Some icky string formatting.
  memset(tmpBuf, 0, sizeof(tmpBuf));
  dtostrf(v_panel, 6, 3, tmpBuf);
  strcat(buf, tmpBuf);
  strcat(buf, ",");
  
  memset(tmpBuf, 0, sizeof(tmpBuf));
  dtostrf(v_charger, 6, 3, tmpBuf);
  strcat(buf, tmpBuf);
  strcat(buf, ",");
  
  memset(tmpBuf, 0, sizeof(tmpBuf));
  dtostrf(v_battery, 6, 3, tmpBuf);
  strcat(buf, tmpBuf);
  strcat(buf, "\n");
  Serial.print(buf);
  _delay_ms(500);
}

/*****************************************************************************
 * setCharger
 * 
 * Determines the correct state of the charging switch
 * based on the solar, charger, and battery voltages.
 *****************************************************************************/
 
void setCharger() {
  
  // Power supply maintenance.
  if (isShutdown()) {
    // The charger is shutdown. Just check the voltage and see
    // if it is lower than we want it to be.

    // Only charge if the sun is up.
    if (getSolarVoltage() > MIN_SOLAR) {
      if (v_battery < MIN_BATTERY) {
        // Turn the charger back on.
        digitalWrite(SHUTDOWN, SHUTDOWN_NO);
      }
    } else {
      // Save a milliamp and turn off the shutdown transistor.
      digitalWrite(SHUTDOWN, SHUTDOWN_NO);
    }
  } else {
    
    // If the sun is up...
    if (getSolarVoltage() > MIN_SOLAR) {

      // Shutdown the charger.
      digitalWrite(SHUTDOWN, SHUTDOWN_YES);
     _delay_ms(100);
     
      // See if battery is charged.
      if (getBatteryVoltage() < MAX_BATTERY) {
        
        // Not charged - don't shut it down.
        digitalWrite(SHUTDOWN, SHUTDOWN_NO);
      }
    } else {
      // Don't keep the transistor on if no sun.
      digitalWrite(SHUTDOWN, SHUTDOWN_NO);
    }
  }
}

int SendCmd(String cmd) {

  int avail;
  uint8_t data;
  
  Serial.print(cmd);
  _delay_ms(1100);
  
  // Get the length
  avail = Serial.available();

  // Empty the input buffer.
  while (Serial.read() != -1);

  // Return with 0 if error response
  if (avail > 4) return 0;

  // Return 1 if "OK\r\n"
  return 1;
  
}

/*****************************************************************************
 * setup
 * 
 * Initializes everything.
 *****************************************************************************/
 
void setup() {
  // Open serial port
  Serial.begin(9600);

  pinMode(SQUAREWAVE, INPUT);
  digitalWrite(SQUAREWAVE, 1);
  
  pinMode(INDICATOR, OUTPUT);
  digitalWrite(INDICATOR, INDICATOR_OFF);
  
  pinMode(SHUTDOWN, OUTPUT);
  digitalWrite(SHUTDOWN, SHUTDOWN_NO);

  pinMode(XBEE_RESET, OUTPUT);
  digitalWrite(XBEE_RESET, XBEE_RESET_YES);
  _delay_ms(1);
  digitalWrite(XBEE_RESET, XBEE_RESET_NO);

  pinMode(SLEEP_PIN, OUTPUT);
  digitalWrite(SLEEP_PIN, SLEEP_NO);

  SendCmd("+++");
  SendCmd("ATSM=5\r");    // Cyclical sleep with pin wakeup.
  SendCmd("ATS0=3\r");    // No I/O reads with wakeup.
  SendCmd("ATST=9C4\r");  // Wait 2.5 seconds from xmit to sleep. 
  SendCmd("ATSP=100\r");  // Sleep 1 second at a time.
  SendCmd("ATCN\r");      // Exit command mode.

  digitalWrite(SLEEP_PIN, SLEEP_YES);
  
  // See if the RTC is present
  if (! rtc.begin()) {
    while (1);
  }
  
#ifdef DS_3231
  if (rtc.lostPower()) {
#else
  if (!rtc.isrunning()) {
#endif
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }
#ifdef DS_3231
  rtc.writeSqwPinMode(DS3231_SquareWave1Hz);
#else
  rtc.writeSqwPinMode(SquareWave1HZ);
#endif
  
  now = rtc.now();
  ticker = now.second();
  
  // Disable timer0
  TIMSK0 &= ~_BV(TOIE0);

  // Enable the interrupt as a falling edge.
  attachInterrupt(digitalPinToInterrupt(2), Int0, FALLING);

  // Change the reference to the 1.1V internal.
    analogReference(INTERNAL);

  // Clear out one read for the ADC reference change.
  analogRead(V_PANEL);
}

/*****************************************************************************
 * loop
 * 
 * Main program loop
 *****************************************************************************/
 
void loop() {

  while(1) {
 
    LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF); 
  
    // Just skip 58 seconds, but after that, read
    // the RTC to get the real second.
    
    if (++ticker > 57) {
      digitalWrite(INDICATOR, INDICATOR_ON);
      digitalWrite(SLEEP_PIN, SLEEP_NO);
      now = rtc.now();
      if (now.second() == 0) {
        
        // Send the voltage readings to the radio.
        saveReadings();
  
        // Set the charging state.
        setCharger();
        digitalWrite(INDICATOR, INDICATOR_OFF);

        // Reset the ticker in case we ran over one second.
        now = rtc.now();
        ticker = now.second();
        digitalWrite(SLEEP_PIN, SLEEP_YES);
      }
    }
  }
}

// The Int0() just needs to be here - it doesn't have to do anything.
void Int0() {;}

            

Reading Data

To read the data from the XBee Explorer or equivalent XBee to USB board, use the following little Python 3 script.

import os.path
import serial

filename = "solar-433.csv"

if not os.path.isfile(filename):
    with open(filename, "w") as of:
        of.write("time,solar,charger,battery\n")
        of.close()

with serial.Serial('/dev/cu.usbserial-A9WFN915', 9600, timeout=None) as ser:
    while True:
        res = ser.readline().decode("utf-8")
        print(res.rstrip())
        of = open(filename, "a")
        of.write(res)
        of.close()
            

Performance

Performance is similar to the 433MHz experiment, in that the battery doesn't discharge enough to tax the solar panel and charger. During bench testing the remote XBee would draw 60mA when it was awake and transmitting, but by the time I got it all configured there was no visible peak in current that approximated 60mA. The current pulses may have been too short for me to pick up, so there may be more actual current usage than I could measure.

Finally

The XBees work great in a low-power system, providing plenty of range and 9600 baud. They don't require huge solar panels or lots of batteries to run continuously. They are a little pricey, but they may be worth it in the long run. The first ones I tested were XB24C7WIT, with wire antennae, but I have some XB24Z7UIT, which use an external antenna. They are currently running, and all seems well. I had to lower the gain of the receiving XBee to get reliable communications, but my receiving antenna is 24 inches from the antennae on the WiFi router.

See Also: Solar Arduino Projects for a list of the solar projects.

Arduino Board Logo

 

Arduino-Board is the go-to source for information on many available Arduino and Arduino-like boards, tutorials and projects.

Help and Support

Arduino-Board

Stay updated

Sign up if you would like to receive our once monthly newsletter.