In my last post, I showed how I was able to set up my low power temperature system using the ATtiny84 IC to help reduce the power down to about 1.2mA, which was lower than the Arduino setup and much lower than my ESP8266 setup.

I’ve now been doing a bit of work trying to reduce the power even further to see how low I can make the power consumption (and therefor how long I could make the system run on a battery power supply).

NRF24L01

The NRF Adafruit library has the ability to power up and power down the NRF module. The code is simple enough, using radio.powerUp() and radio.powerDown() function call.

Strangely, by itself, this didn’t seem to save any power, with the non-transmitting current draw staying at around 1.04mA. This could be because the NRF module is only transmitting and not listening, so in theory, it shouldn’t draw much power when it’s not transmitting.

NRF24L01 Power Down Current Reduction: 0mA

MCP9808

As with the NRF24L01 module, the MCP9808 module can also be put into low power mode when it’s not being used.

These functions are built into the Adafruit MCP9808 library, but since I’m bit-banging the I2C interface, I had to copy and slightly modify the functions into my own code. This was simple enough, and the code is shown in the example code at the bottom.

As with the NRF module, when I want to obtain a reading from the MCP module, I wake it up, obtain the temperature, and then put the module back to sleep.

This was more successful than the NRF module power down, saving about 0.12mA during the non-transmission phase.

MCP9808 Power Down Current Reduction: 0.12mA

ATTiny84

The ATtiny84 module can also be put into power-saving mode, but this is a bit more complicated, with various different options available depending on your needs.

WDT Sleep Mode

For my system, I only need to obtain and transmit the temperature once a minute, allowing the system to sleep between read-send cycles. Therefore, I can put the system to sleep, and use the WDT to interrupt the sleep and resume the code.

To get this to work, I use the follow code:


#include <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/interrupt.h>

void setup_watchdog(int ii) 
{
  // 0=16ms, 1=32ms, 2=64ms, 3=128ms, 4=250ms, 5=500ms
  // 6=1 sec,7=2 sec, 8=4 sec, 9=8 sec

  uint8_t bb;
  if (ii > 9 ) ii=9;
  bb=ii & 7;
  if (ii > 7) bb|= (1<<5);
  bb|= (1<<WDCE);

  MCUSR &= ~(1<<WDRF);
  // start timed sequence
  WDTCSR |= (1<<WDCE) | (1<<WDE);
  // set new watchdog timeout value
  WDTCSR = bb;
  WDTCSR |= _BV(WDIE);
}

// system wakes up when watchdog is timed out
void system_sleep(int ii) 
{
  setup_watchdog(ii);                   // approximately 8 seconds sleep
 
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // sleep mode is set here
  sleep_enable();
  sei();                               // Enable the Interrupts so the wdt can wake us up

  sleep_mode();                        // System sleeps here

  sleep_disable();                     // System continues execution here when watchdog timed out 
}

// ...
sleep(9);    // Causes the code to sleep for 8 seconds
// ...

The sleep(9) code is called after the temperatures have been read and sent, causing the ATTiny84 to go into sleep mode for 8 seconds (this will be increased later using a counter to get it up to 1 minute, but this will do these tests).

Using this simple WDT sleep mode, the non-transmitting phase current is reduced by 0.6mA, the biggest increase in current so far.

ATtiny84 WDT Sleep Current Reduction: 0.6mA

PortA and PortB Pull-Up Resistors

Small amounts of power can be saved by pulling up all unused ports high. Since during sleep mode, none of the ports are required, I simply add code in the sleep function to make all ports input and send them high.


  DDRA = 0x00;  // Set direction to input on all pins 
  PORTA = 0xFF; // Enable pull-ups on pins 
  DDRB = 0x00;  // Set direction to input on all pins 
  PORTB = 0xFF; // Enable pull-ups on pins 

This saved about 0.1mA

ATtiny84 Pull All Ports High Current Reduction: 0.1mA

ADC Off

For my project, I’m not using the ADC at all, so I’m happy to disable it during the project setup using the code:

ADCSRA &= ~ bit(ADEN); // disable the ADC
bitSet(PRR, PRADC); // power down the ADC

This helped save about 0.22mA of current.

ATtiny84 Disabling ADC Current Reduction: 0.22mA

Timer 1

For my system, I’m only using Timer0, so I can disable Timer1 using the code:

PRR = bit(PRTIM1); // Turn timer1 off

This helped save about 0.1mA of current.

ATtiny84 Disabling Timer1 Current Reduction: 0.1mA

USI

During sleep, USI is not needed, so I can turn it off. In fact, I now only turn it on when the NRF module is enabled, as the I2C is bit-banged, so the USI is not needed.


bitClear(PRR, PRUSI); // enable USI h/w
radio.powerUp();
while(!radio.write(&c, sizeof(c))){
}
radio.powerDown();
bitSet(PRR, PRUSI); // disable USI h/w

This helped save about 0.1mA of current.

ATtiny84 Disabling USI Current Reduction: 0.1mA

Altogether

When I combine all the about power saving features (including the NRF module, the MCP module, and all the ATTiny power saving features), I found that I was able to get the current down to about 0.01mA!

However, at some points, my multi-meter shows the current as 0.00mA, so I’m now beyond the capabilities of my multi-meter to measure the current draw.

However, despite this, it’s clear that I’ve been able to create a usable wireless temperature sensor that during sleep, only draws tens of micro-amps. Personally, I’m pretty happy with that!

Arduino Transmitter Code

Below is the temperature sensing and transmitting code.


/*
 * Low Power Weather Station V0.5
 * weatherStationTX
 * 
 * main.c
 * 
 * Using various low power methods to get the power-down current to roughly 10uA.
 */
 
#include "RF24.h"
#include "SlowSoftI2CMaster.h"
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/interrupt.h>

/****************** User Config ***************************/
#define CE_PIN 8
#define CSN_PIN 7
#define scl 10
#define sda 9

#define READ_DELAY 250

// Device addresses currently being used
const uint64_t pipes[3] = { 0xF0F0F0F0E1LL, 0xF0F0F0F0E2LL, 0xF0F0F0F0E3LL };

// MCP9808 codes and addresses
#define MCP9808_I2CADDR_DEFAULT        0b0011000
#define MCP9808_REG_CONFIG             0x01

#define MCP9808_REG_CONFIG_SHUTDOWN    0x0100
#define MCP9808_REG_CONFIG_CRITLOCKED  0x0080
#define MCP9808_REG_CONFIG_WINLOCKED   0x0040
#define MCP9808_REG_CONFIG_INTCLR      0x0020
#define MCP9808_REG_CONFIG_ALERTSTAT   0x0010
#define MCP9808_REG_CONFIG_ALERTCTRL   0x0008
#define MCP9808_REG_CONFIG_ALERTSEL    0x0004
#define MCP9808_REG_CONFIG_ALERTPOL    0x0002
#define MCP9808_REG_CONFIG_ALERTMODE   0x0001

#define MCP9808_REG_UPPER_TEMP         0x02
#define MCP9808_REG_LOWER_TEMP         0x03
#define MCP9808_REG_CRIT_TEMP          0x04
#define MCP9808_REG_AMBIENT_TEMP       0x05
#define MCP9808_REG_MANUF_ID           0x06
#define MCP9808_REG_DEVICE_ID          0x07

// MCP9808 functions
uint8_t _i2caddr;

/* Hardware configuration: Set up nRF24L01 radio on SPI bus plus pins 9 & 10 */
RF24 radio(CE_PIN, CSN_PIN);
SlowSoftI2CMaster si = SlowSoftI2CMaster(sda, scl);
/**********************************************************/

/****************** Sleep functions ***************************/

// Sets up the WDT
void setup_watchdog(int ii) 
{
  // 0=16ms, 1=32ms, 2=64ms, 3=128ms, 4=250ms, 5=500ms
  // 6=1 sec, 7=2 sec, 8=4 sec, 9=8 sec

  uint8_t bb;
  if (ii > 9 ) ii=9;
  bb=ii & 7;
  if (ii > 7) bb|= (1<<5);
  bb|= (1<<WDCE);

  MCUSR &= ~(1<<WDRF);
  // start timed sequence
  WDTCSR |= (1<<WDCE) | (1<<WDE);
  // set new watchdog timeout value
  WDTCSR = bb;
  WDTCSR |= _BV(WDIE);
}

// System wakes up when watchdog is timed out
void system_sleep(int ii) 
{
  setup_watchdog(ii);                   // approximately 8 seconds sleep
 
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // sleep mode is set here
  sleep_enable();
  sei();                               // Enable the Interrupts so the wdt can wake us up

  DDRA = 0x00;                          // Set direction to input on all pins 
  PORTA = 0xFF;                         // Enable pull-ups on pins 
  DDRB = 0x00;  
  PORTB = 0xFF;
  sleep_mode();                        // System sleeps here

  sleep_disable();                     // System continues execution here when watchdog timed out 
}

/**********************************************************/

/****************** MCP functions ***************************/

void write16(uint8_t reg, uint16_t value) {
  si.i2c_start_wait((_i2caddr << 1) | I2C_WRITE);    //Wire.beginTransmission(_i2caddr);   si.i2c_write((uint8_t)reg);   si.i2c_write(value >> 8);
  si.i2c_write(value & 0xFF);
  si.i2c_stop();
}

uint16_t read16(uint8_t reg) {
  uint16_t val;
  
  si.i2c_start_wait((_i2caddr << 1) | I2C_WRITE);
  si.i2c_write((uint8_t)reg);
  //si.i2c_stop();
  
  si.i2c_rep_start((_i2caddr << 1) | I2C_READ);
  val = si.i2c_read(false);
  val <<= 8;
  val |= si.i2c_read(true);
  si.i2c_stop();
  return val;  
}

boolean begin(uint8_t addr = MCP9808_I2CADDR_DEFAULT) {
  _i2caddr = addr;
  si.i2c_init();

  if (read16(MCP9808_REG_MANUF_ID) != 0x0054) return false;
  if (read16(MCP9808_REG_DEVICE_ID) != 0x0400) return false;

  write16(MCP9808_REG_CONFIG, 0x0);
  return true;
}


float readTempC( void )
{
  uint16_t t[4];
  read16(MCP9808_REG_AMBIENT_TEMP);       // Removes the value stored just before shutdown
  delay(READ_DELAY);
  // Getting the average of 4 temperature values.
  for (byte i = 0; i < 4; i++){
    t[i] = read16(MCP9808_REG_AMBIENT_TEMP);
    delay(READ_DELAY); 
  }
  
  float temp[4];
  for (byte i = 0; i < 4; i++){
    temp[i] = t[i] & 0x0FFF;
    temp[i] /=  16.0;
    if (t[i] & 0x1000) temp[i] -= 256;
  }
 
  return (temp[0] + temp[1] + temp[2] + temp[3]) / 4;
}

void shutdown_wake( uint8_t sw_ID )
{
    uint16_t conf_shutdown ;
    uint16_t conf_register = read16(MCP9808_REG_CONFIG);
    if (sw_ID == 1)
    {
       conf_shutdown = conf_register | MCP9808_REG_CONFIG_SHUTDOWN ;
       write16(MCP9808_REG_CONFIG, conf_shutdown);
    }
    if (sw_ID == 0)
    {
       conf_shutdown = conf_register & ~MCP9808_REG_CONFIG_SHUTDOWN ;
       write16(MCP9808_REG_CONFIG, conf_shutdown);
    }
}

void shutdown(void)
{
  shutdown_wake(1);
}

void wake(void)
{
  shutdown_wake(0);
  //system_sleep(4);//delay(250);
  delay(250);
}

// Setup look
void setup(){  
  while (!initRadio()){}
  while (!initMCP()){}
  DDRB = 0x00;            // Set direction to input on all pins 
  PORTB = 0xFF;           // Enable pull-ups on pins 


  #define BODS 7          //BOD Sleep bit in MCUCR
  #define BODSE 2         //BOD Sleep enable bit in MCUCR
  MCUCR |= _BV(BODS) | _BV(BODSE); //turn off the brown-out detector

  PRR = bit(PRTIM1);      // Turn timer1 off

  ADCSRA &= ~ bit(ADEN);  // disable the ADC
  bitSet(PRR, PRADC);     // power down the ADC
}

// main loop
void loop(){
  wake();
  float c = readTempC(); 
  shutdown();

  bitClear(PRR, PRUSI); // enable USI h/w
  radio.powerUp();
  while(!radio.write(&c, sizeof(c))){
  }
  radio.powerDown();

  system_sleep(9);        // Sleep for 8 seconds
}

bool initRadio(){
  radio.begin();

  // Set the PA Level low to prevent power supply related issues since this is a
  // getting_started sketch, and the likelihood of close proximity of the devices. RF24_PA_MAX is default.
  radio.setPALevel(RF24_PA_LOW);

  // Open up pipe for writing
  radio.openWritingPipe(pipes[0]);
  
  radio.stopListening();

  return true;
}

bool initMCP(){
  return begin();
}

Next Steps

Now that I’ve got my current consumption down to a level I’m happy with, my next step is to create a prototype board containing everything required to connect the wireless temperature sensor to a battery.

Advertisements