ATtiny84 temperature sensor power consumption

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

ATtiny84 wireless temperature sensor

In my previous post, I discussed the creation of a low-power, wireless temperature sensor using the Arduino Uno. As mentioned, the current draw was relativaly high about 22mA, most of which was being drawn from the Arduino.

Therefore, I have worked on updating the system so that it uses an attiny84 instead of the Arduino, which should allow me to significantly reduce my current draw, which shoudl help increase the batter life of my weather station.

ATtiny84

The ATtiny84 is a 14 pin, 8-bit AVR which is also part of AVRs Microchip’s picoPower, which apparently allow a current draw as low as 0.1 μA (at 1.8V). Although Im working at 3.3v (ish, repending on battery voltage), the ATtiny should still give me a very low power drain.

The AVR has SPI and I2C interfaces, but after a while, I learned that both work off the same bus, so I ended up using the built-in SPI for the NRF module and use software bit-banging for the MCP9808’s I2C interface.

The NRF24L01 SPI interface worked as expected with no further changes required (thanks to the write-up here: https://tmrh20.github.io/RF24/ATTiny.html)

The MCP9808 I2C interface was more of a pain! Initially I tried using the same code as the Arduino, but at the time, I didn’t realise that the SPI and I2C interface worked off the same bus. Afdter various attempts, I finally figured out that using the SlowSoftI2CMaster library worked in parallel to re-creating the Adafruit MCP9808 library using the soft I2C library.

This worked well and I am now have a system setup where the ATtiny84 reads the temperature from the MCP9808, transmits it to a receiver Arduino using a pair of NRF24L01 modules, and then waits 10 seconds before doing it all over again.

Current Draw

At the moment, the system does not have any power shut-down modes enabled, so that power efficiency is not as good as it could be. However, even in its current state, the transmitter circuit has been measured at around 1.2mA, which is about 5.5% of the Arduino current draw.

Even as it is, Im pretty happy with that, but in the next update, I will start to implement power saving modes to see how low little current I can draw.

Code

The code for the ATtiny84 is currently written and programmed via the Arduino IDE and an Arduino Uno (via Arduino SPI). The code is below incase anyone finds it useful.



/*
 * Low Power Weather Station V0.4
 * weatherStationTX
 * 
 * main.c
 * 
 * Using Attiny84 instead of Arduino
 */
 
#include "RF24.h"
#include "SlowSoftI2CMaster.h"

#define CE_PIN 8
#define CSN_PIN 7
#define scl 10
#define sda 9

/****************** User Config ***************************/

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

/***************** Define functions used ******************/
bool initRadio();
bool initMCP();
/**********************************************************/

// 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;

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;
}

#define READ_DELAY 250
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;
}

// Setup look
void setup(){  
  while (!initRadio()){}
  while (!initMCP()){}
}

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

  while(!radio.write(&c, sizeof(c))){
  }

  // Generic delay (probably more in the final device).
  delay(10000);
}

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();
}

 

Low Power, Wireless Temerature Sensor

It’s been a long while since I gave an update on my projects, so I thought it would be good to have a quick update on one of the projects I am currently working on.

A while back, I added an outside temperature sensor using the ESP8266 wifi module. Although the module worked fine, I recently started to find that the power consumption was higher than desired and that the batteries (2 AA batteries) were being drained quicker than I would like and that I was having to replace / recharge the batteries every week or two. Although I’m sure some of the problem is to do with the cold we have been having lately here in the UK, I’m also sure that some of the problem is to do with the ESP8266 wifi module (and wifi modules in general) not being very battery friendly, mainly due to the high current required when the device is not in sleep mode (and the releativly high currentl draw when it is in sleep mode…).

Therefore, I decided to look into a more power efficient system.

As stated above, the greatest power draw is the ESP8266 wifi module. Therefore, this was the first thing to look into changing.

NRF24L01 2.4GHz SPI Module

I have decided to go with the nrf24l01 2.4GHz, mainly due low power requirements, which are about:

  • 13mA during transmission
  • 900nA in shutdown mode

This beats anything I could hope to come close to using the ESP8266.

The board is simple enough to use on an Arduino-like board using the RF24 library available here: https://github.com/nRF24/RF24

Using the above library, it’s simple enough to connect the NRF board to an arduino (I’m currently testing on an Arduino Uno, though I will have to change this as I’m aware it’s not great when it comes to low power consumption, but it’s nice and easy to test with). There are plenty of instructions on setting up the nrf module with an arduino, so I won’t go into details here.

MCP9808 Temperature Sensor I2C Module

For the temperature sensor, Im using the MCP9808 i2c temperature sensing IC, which is currently on an Adafruit breakout board.

Again, this was chosen because of its low power consumption:

  •  ~ 200 μA during operation
  • ~ 0.1 μA during sleep.

As with the rf module, this was simple enough to setup and on the arduino using the instructions here: https://learn.adafruit.com/adafruit-mcp9808-precision-i2c-temperature-sensor-guide/overview

Simple enough, so not much to write about.

Arduino

For this stage, I am using an Arduino Uno because it’s simple to setup and test everything works well. So far, everything does seem to work well and I have been able to create a small system containing 1 transmitter and 1 receiver. The receiver is currently setup so that it could receive multiple signals (up to 6), but as of yet, I have not implemented them.

The code the receiver is below


/*
 * Low Power Weather Sheild V0.2
 * mainRecevierRX
 * 
 * main.c
 */

#include 
#include "RF24.h" 

/****************** User Config ***************************/

/* Hardware configuration: Set up nRF24L01 radio on SPI bus plus pins 9 & 10 */
RF24 radio(9,10);
/**********************************************************/

const uint64_t pipes[3] = { 0xF0F0F0F0E1LL, 0xF0F0F0F0E2LL, 0xF0F0F0F0E3LL };
/*
 * 0xF0F0F0F0E1LL = Weather station
 * 0xF0F0F0F0E2LL = Livingroom - not currently used
 * 0xF0F0F0F0E3LL = UNUSED
 */

void setup(){
  Serial.begin(115200);
  Serial.println("Receiver station starting up");

  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.openReadingPipe(1, pipes[0]);
  radio.openReadingPipe(2, pipes[1]);

  radio.startListening();
}

void loop(){
  uint8_t pipe;
  float buf1;
  unsigned char buf2;
  if (radio.available(&pipe)){
    if (pipe == 1){
      radio.read(&buf1, sizeof(buf1));
      //Serial.print("Weather station");
      //Serial.print(": ");
      Serial.println(buf1);
    }
    if (pipe == 2){
      radio.read(&buf2, sizeof(buf2));
      Serial.print("Livingroom");
      Serial.print(": ");
      Serial.println(buf2);
    }
  } 
}

The code for the transmitter is:


/*
 * Low Power Weather Station V0.2
 * weatherStationTX
 * 
 * main.c
 */

#include 
#include "RF24.h"
#include 
#include "Adafruit_MCP9808.h"

/****************** User Config ***************************/

/* Hardware configuration: Set up nRF24L01 radio on SPI bus plus pins 9 & 10 */
RF24 radio(9,10);
// MCP9808 temperature sensor object
Adafruit_MCP9808 tempSensor = Adafruit_MCP9808();
/**********************************************************/

/***************** Define functions used ******************/
bool initRadio();
bool initMCP();
/**********************************************************/

const uint64_t pipes[3] = { 0xF0F0F0F0E1LL, 0xF0F0F0F0E2LL, 0xF0F0F0F0E3LL };

void setup(){
  Serial.begin(115200);
  Serial.println("Weather station starting up");
  Serial.println("Now contains MCP9808 temperature sensor (in testing)");

  while (!initRadio()){}
  while (!initMCP()){}
}

void loop(){
  float c = 0;

  // Obtaining the temperature.
  tempSensor.wake();
  c = tempSensor.readTempC();  
  Serial.println(c);
  tempSensor.shutdown();
  
  // Sending the temperature via radio
  radio.powerUp();
  while(!radio.write(&c, sizeof(c))){
    Serial.println("Sending signal failed");
  }
  Serial.print("Sending temperature: ");
  Serial.println(c);
  radio.powerDown();

  // Generic delay (probably more in the final device).
  delay(60000);
}

bool initRadio(){
  Serial.println("Setting up radio.");
  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();
  radio.powerDown();

  return true;
}

bool initMCP(){
  Serial.println("Setting up MCP.");
  if (!tempSensor.begin()){
    Serial.println("Could not find MCP9808!");    
    return false;
  }
  tempSensor.shutdown();
  return true;
}

Ive connected up the arduino circuits and in my simple test, the temperature is being sent once a minute from the transmitter to the receiver, with the receiver signal being sent to the serial port for plotting.

The code is certainly not perfect, and when I connect it to the power supply, it is drawing 22mA when at rest. However, I’m aware that most of this current will be being drawn from the arduino, so I’m not too bothered by that.

 

Next steps

Now that the programs are working as expected, my next step is to recreate the arduino circuit using a low power attiny84. This should allow me to massively reduce the power consumption of the transmitter. I will look into connecting the receiver to a Raspberry Pi Zero W which will then be connected to the local network to send  data via MQTT.

I’m interested to see how low I can go in terms of power consumption for the transmitter!

Room temperature control system – P(ID) and PWM

Now autumn is setting in, I’ve started doing some work on my control algorithms for my home temperature control system.

I had been using a simple On-Off (“bang-bang”) control system, but as expected, I was getting quite large temperature overshoots above the desired temperatures. In some cases, I was getting an overshoot of about 2C, which at times made things quite uncomfortable.

Since I had already done quite a bit of work on temperature control in other systems, I decided to improve on the system using a control methodology that has worked well for me in the past: PID control.

PID controller

For those that don’t know what PIC control is, there’s plenty of tutorials and examples on the internet, but a simple explanation is that is uses Proportional, Integral, Derivative errors between the desired state (temperature) and the actual state (temperature) to decide the input of the system. For my controller, the input is the amount of time the boiler is on for (I’m using PWM and will explain that more later), so the higher the number (the greater the errors), the greater the time the boiler is one for.

  • Proportional – the output is proportional to the error. For this controller, if the temperature error is higher, the boiler is switched on for longer.
  • Integral –  the output is proportional to the integral of the error. If the temperature stays too low for a long period, the integral of the error will go up, so the boiler will start to stay on for longer. This helps get rid of steady state errors.
  • Derivative – the output is proportional to the slope of the error. So if the error is getting small too fast (i.e. the temperature is rising too quickly) the boiler will switch on less. This will help reduce overshoot and to compensate for the time-delay, which can cause big issues in temperature control systems (when rooms can still get hot after the temperature source has been switched off).

For now, I’m only using P, effectively keeping D and I at zero (though in practice, I’m simply not calculating them to make life easy in the short term while I play with the system).

So, for my system, the output of the controller is simply

output = (desired temperature – actual temperature) * P

For now, I’ve got P = 100, D = 0, I = 0

PWM controller

However, my boiler doesn’t understand any of the numbers given by the PID controller above, instead only understanding ON or OFF. The control to the boiler simply consists of a esp8266 board controlling a mechanical relay, which then replaces the thermostat controller on the combi-boiler. In other words, I can only switch my heater on or off.

Therefore I’m using PWM (pulse width modulation) to convert the PID’s output into the on-off control that can be understood by the heater.

Again, I won’t go into too much detail into PWM controllers as there are better tutorials out there than I could write. When my PID controller gives out 100, the PWM system will have the heater on at 100% and when it’s at 0, the heater will be at 0. Anything else is simply a percentage between 0% and 100%.

For those interested, I chose a PRF (pulse repetition frequency) of 10 minutes and a period of 1 minute. Therefore, a PID out of 13% and 18% would have the heater stay on for 2 minutes, while a PID output of 2% would have the heater on for 1 minute. I’m sure I’ve done something wrong here and 13% should keep the boiler on for 1 minute (not 2 minutes), but it’s okay for now.

Results

I’ve only had the new system up and running for about a week, but so far it seems to work well. I now only have an overshoot of about 0.2C compared to about 2C. The system seems quick enough and I’ve yet to notice any major problems with it.

Below you can see an example screen shot of the last 24 hours showing the desired temperature along with the actual temperature. This is only the temperature in my living room, which is currently the only room to contain a temperature sensor (though that will change soon enough). The bottom graph shows when the boiler is on or off (1 being on, 0 being off).

wp-1476883910397.png

All the controlling is done through node-red and the user-interface is done via node-red-dashboard. If anyone would like more details on the node-red flows, feel free to ask.

Where next?

One thing I want to play with next is figuring out the best PRF value. I would like something smaller than 10 minutes, but I’m not sure how well the heater would cope with constantly being switched on and off. If anyone has any ideas or thoughts, I’d be happy to hear them.

I’m also going to look at introducing the D part of the PID controller to try and control any potential overshoots caused by big temperature changes.

 

Updated OLED Code

Tags

,

I’ve been trying to improve the look of the display, but I seem to be coming up to number of issues with the ESP_SSD1306.h library, mainly:

  • There’s no easy way to center or change the left / right justification of the text.
  • I can’t seem to get the width of a string.
  • It seems quite slow (though this could just be the way I’m using it…).

Therefore I decided to have another look at the esp8266-oled-ssd1306.h library which I did look at a while ago, but I found the example quite long and difficult to follow.

Well it turns out that the library itself is actually quite easy to use. I’m not sure why they have only make 1 very large example that makes it look much more difficult than it needs to be. I have no need of frames, transitions, etc. However, after playing around with it, I seem to have been able to overcome the issues I was having with the ESP_SSD1306.h library:

  • I can center text with ease. I can also change the left / right justification of text with a single line of text.
  • Getting the width of a string is really easy (only take 1 function to get the width and height of a given string).
  • It does seem quicker than what I was using before (but again, this could be a trick as I’ve not tried to actually measure anything).

I’ve not updated my code to use this other library and everything seems to be going well. I think I’ll keep using it in the future for this project.

You can see the updated display below.

img_20160401_164304.jpg

Image of latest display design

ESP8266 Thermostat -First ESP-12E Circuit

Since I’ve now got the back-none of the software created, I’m now in the process of designing and building the circuit that will be used in the final system.

Things are still currently on breadboard, but surprisingly the circuit seems to be working well!

The circuit comes from Kolban’s Book on the ESP8266 and ESP32 which gives a good circuit for programming the ESP-12E board (and ESP-1). I’m using the Arduino IDE to build and flash the “sketch” and so far it seems to be working well. The book is certainly worth a read and it has made me want to start programming using the Eclipse SDK, but maybe another day.

I’ve had to adapt the circuit for the I2C chips and the rotary encoder. The circuit diagram can be seen below, but I’ve also uploaded the EagleCAD schematic to my Github project page if anyone want to use it. There’s no PCB layout at the moment as I’m still working on improving the circuit on the breadboard.

Full Circuit Diagram

Circuit diagram for wireless temperature sensor

This all appears to be working on my breadboard, but I wish the ESP-12E was much more breadboard friendly than it is! Given all the messy wires from the board, I’m amazed that I actually managed to get it working for about a week without any issues…

img_20160401_151140.jpg

Breadboard circuit.

Anyway, above you can see my messy breadboard circuit. As you can see the ESP-12E really isn’t breadboard friendly…

I was tempted to go veroboard next, but I think I’ll go straight to making a single sided, milled PCB. So need to get started on creating a few more Eagle library components as I still can’t find one for the OLED module!

Wemos D1 Mini Boiler Switch

As well as recording the current temperature, one of the more important abilities of any thermostat system is the ability to turn the turn the boiler on and off as required.

Now I’ve played with a few “off the shelf” thermostats and really didn’t like any of them!

Therefore the obvious thing to do is create my own 🙂

I tried a few times to get my current SSR to turn my boiler on / off, but I wasn’t getting anywhere with it. It turns out that’s because I was using a zero-cross sensing SSR, which just wouldn’t work.

Therefore I looked into buying another SSR to test on my system.

Wemos D1 Mini

And then I came across the Wemos D1 Mini. This is a nice little ESP-8266EX unit, built into a stackable system. It’s much smaller than the NodeMCU boards, but still contains everything to program the board via microUSB (so no serial adapters, etc.)

miniYou can see the board above, and more details can be found here: http://www.wemos.cc/Products/d1_mini.html The board is available for just under £3, which is nice and cheap for me.

Wemos D1 Mini Relay Shield

However, what I liked about this board, was that I could buy a Relay Shield to sit on top of the ESP board.

relay

You can see the board above, and more details can be found here: http://www.wemos.cc/Products/relay_shield.html

Combined together, you have a MQTT based 240v relay switch for £4.18

There might be cheaper options out there, but I do like the the look of this one. It’s easy to build, cheap enough, and stackable (which I can’t see me needing any more than the relay switch at the moment).

Boiler Switch Circuit

The circuit for this is simple enough. Just plug in a microUSB to power the board and connect the relay pins to the required pins / cables on your boiler. This will of course be different for different boilers.

BE VERY CAREFUL AROUND 240 / 120V LIVE WIRES. I’M NOT RESPONSIBLE IF YOU’RE STUPID ENOUGH TO KILL YOURSELF BY NOT KNOWING WHAT YOUR DOING!!!

Without that out of the way, the code is simple enough and is available in my Github project folder, under “Boiler Switch”. This can be found here: https://github.com/nerobot/Home-Automation/tree/master/ESP8266%20boiler%20switch

ESP Temperature Sensor – “Sleeping”

I’m getting closer to creating – what is to me – a useful wall-mounted wireless temperature sensor using the ESP8266 module.

However, one thing that I didn’t want for this system is the display to be on all the time. The blue light from the OLED isn’t that nice and I don’t really need to know the temperature at every moment.

Instead, it would be good if the display only came on when someone is trying to change the display. More specifically, it would be good if the display only came on when the rotary encoder was being used and for about 60 seconds after the last turn.

So that’s what I’ve done!

FSM

I’ve updated the VERY simple FSM to include two more states and quite a few more transitions. Below is a messy diagram showing the state, the triggers, and the transitions for the new FSM. The letters in boxes correspond to the square brackets as explained in the transitions below.

FSM_26Mar16.jpg

Diagram of FSM

States

I now have two states in this FSM: waiting & sleeping

  • waiting
    • This is when the display is on.
    • The display should stay on for 60 seconds after the last change in the rotary encoder.
    • The display will show the latest temperature and the latest desired temperature. If either of these change in this state, the screen is updated.
  • sleeping
    • This is when the display is off.
    • This state will happen 60 seconds after the last change in the rotary encoder.
    • If the encoder is changed during this state, the FSM moves to waiting.
    • If the temperature or MQTT desired temperature are updated in this state, the new values are stored, but nothing is shown on the screen. The screen will remain blank.

Triggers

The triggers for the new FSM are explained below.

  • TRIGGER_TEMPERATURE_WAIT
    • Happens every 60 seconds.
    • Causes the system to get the current temperature.
  • TRIGGER_60_SECONDS
    • Happens every 60 seconds when in the waiting state.
    • Causes the device to go from waiting to sleeping.
  • TRIGGER_ENCODER_CHANGE
    • Happens when the encoder has changed.
    • Causes the system to come out of sleep mode or:
    • Causes the system to obtain a new desired temperature.
  • TRIGGER_NEW_MQTT_DESIRED_TEMP
    • Happens when a new desired temperature is sent to the device via MQTT.

Transitions

I now have a number of new transitions are shown below. The numbers in the square brackets correspond to the square numbers on the FSM diagram.

  • transition_waiting_to_sleeping [1]
    • Turns the display off
  • transition_encoder_move_from_sleeping [2]
    • Turns the display back on
  • transition_obtain_temp_display [3]
    • Obtains the new temperature and displays it
  • transition_new_desired_temp_mqtt [4]
    • Updates the new desied temperature and displays it
  • transition_obtain_temp_display [5]
    • Takes the new temperature from the encoder and displays it
  • transition_obtain_temp [6]
    • Obtains the new temperature, but doesn’t display it
  • transition_new_desired_temp_mqtt_sleeping [7]
    • Updates the new deisred temperature, but doesn’t display it.

Outcome

The code for this is still VERY messy, but as far as I can tell, it all appears to be working well. I’ve uploaded a short video to Youtube showing the device working. I’ve changed some of the times to make it easier to see (time until going to sleep is set as 10 seconds, temperature update interval set to 5 seconds).

You can see the video here Sorry for the terrible video quality…

Next Step

My next step is to start improving the circuit used. I’m going to start designing PCB that uses the ESP-12E board. That way I won’t be relying on the NodeMCU board, but this does mean that I have to design more of the circuit myself. That shouldn’t be an issue, but the ESP-12E isn’t the most breadboard friendly device!

ESP8266 Rotary Encoder

One of the things I’m currently working on is the ability to control the thermostat
“set point” both via a web-based UI and via the wall mounted thermostat itself.

The web-based UI is currently being created in Node-Red (more on that in a later entry), but the wall-mounted thermostat is currently working on an ESP8266. The idea is to have the following setup for the thermostat:

  • The boiler on / off is still controlled by Node-Red
  • The desired temperature can still be set using the web-interface from Node-Red.
  • The desired temperature can also be controlled directly from the wall-mounted thermostat.
  • The wall-mounted thermostat will contain:
    • An ESP8266 which will communicate with Node-Red via MQTT
    • A rotary encoder to control the current desired temperature
    • A OLED display to show the current and desired temperature

I already have the temperature sensor working (and being sent via MQTT to node-red), and the OLED seems to be setup and working. Therefore, I’m currently working on getting the rotary encoder working on the ESP8266.

The encoder I’m currently working on is the KY-040, which is a rotary encoder and a push button in one. This will allow me to choose the desired current temperature (and later use the push button to go through menus, etc.). They are cheap enough (about 40p each), but as I quickly found out, READY bouncy.

Hardware

At the moment, I don’t have the button connected up, but the rotary encoder pins are connected to GPIO 4 and GPIO 5. Below shows the diagram of the whole setup to date, which now includes:

  • ESP8266 nodemcu board
  • DS1631 i2c temperature sensor IC
  • 0.96″ OLED i2c display
  • KY-040 rotary encoder
Rotary Encoder Simplified Circuit Diagram

Simplified circuit diagram of system (excluding ESP setup).

I’ve left out most of the connection details for the ESP8266 as I’m using the nodemcu board which has all this setup (and I can’t actually find an EagleCad library that contains this particular board.

Simple enough and hopefully nothing too complicated. I’m sure there are ways I could clean up the circuit, but this will do for now.

Software

This one took my quite a bit longer than getting the hardware sorted out!

As this was my first attempt at using (cheap) rotary encoders, I stupidly thought that things would be quite easy to get working…

Attempt 1

My first attempt involved simply connecting one of the rotary encoders to a FALLING interrupt call, check the value of the other RE connection, decide if the rotary encoder had moved clockwise or anti-clockwise, and then print to serial within the interrupt call.

School-boy error! Don’t print to serial within interrupt calls. As should have been expected, the program crashed and reset everytime I turned the RE.

Attempt 2

So, second attempt was the same as above, but print to serial outside of the interrupt call.

This stopped the program from crashing, but as I did expect, the RE contacts were VERY bouncy, and the calculated position was all over the place! When turning clockwise, the calculated position was all over the place!

Getting better, but still no good.

Attempt 3

For the third attempt, I decided to get rid of the interrupt called, and poll the RE inputs. This allowed me to:

  • Easily introduce de-bouncing into the code.
  • Know if each input was pressed, had just been pressed, or had just been released.

The code for this was quite similar to what I had done for a previous project. In the git-hub project, the function for this is called checkButtons() I’m sure it could be refined, but for now it works.

For debouncing, I’m currently using 5ms, which seems to be working.

To check for the rotation of the RE, I used the same method as above. Meaning I’d check CLK for a fall (i.e. high to low) and check the value of the DT pin. If DT is high, then increase the position by 1 (i.e. clockwise). If DT is low, then reduce the position by 1 (i.e. anti-clockwise).

This seemed to work. When I turned the RE clockwise, I’d get a nice update in the number, but when I turned it anti-clockwise, it sometimes worked and sometimes didn’t work. Also, when I turned the RE fast, the numbers were all over the place, sometimes going back, sometimes forward. So getting better, but not quite there yet.

Also, increasing the debounce wait time didn’t seem to help, so I was starting to get a bit confused and had to do a bit more research.

Rotary Encoder LED Test

This got me thinking it would be good to test the RE more and see how it was actually working!

To do this, I connected the LEDs to the RE and rotated in both directions. What I found was that for 1 “click” in rotation, I had the following sequences:

  • Clockwise:
    • CLK {0 1 1 0}
    • DT   {1 1 0 0}
  • Anti-clockwise:
    • CLK {1 1 0 0}
    • DT   {0 1 1 0}

Now things are starting to make sense and I’m able to figure out some of my issues!

Attempt 4

So, now I know the pulse sequence for the RE, I was able to create a simple piece of code that stores the last 4 CLK and DT values, and update this each time the RE is rotated.

Then, only increase or decrease the current position after the correct set of sequences.

Upload the code and…

IT WORKS!!!

No bouncing problems. The values only go up when they should go up and only go down when they should go down. If I rotate the encoder, it sometimes doesn’t update. However, this is better than going in the wrong direction!

Progress

After working on the code a bit more, I now have a system where I can:

  • Choose the current desired temperature using the rotary encoder, in steps of 0.5C.
  • Whenever the desired temperature changes, it is sent to node-red via MQTT.
  • Whenever the desired temperature is changes in node-red, it also sends the new value to the ESP via MQTT.

This took a lot longer than it should, but it seems to be working well for now.

You can see the REALLY messy circuit below. The OLED shows the current temperature (in the larger text) and the desired temperature (in smaller text further down).

The code and circuit diagram for this should now be available on the Github project page.

img_20160320_160230.jpg

Circuit including OLED and Rotary Encoder

Next step

The next step is to add some functionality to the button on the rotary encoder. It would be good to get more details from the HA on node-red to display to the OLED (such as the current “Scene”, a menu).

ESP8266 Temperature Sensor – OLED

My new OLED screen has finally turned up, so I’ve been playing with ways of adding a the small display to my wireless temperature sensor.

For now I’ve only been working on using the display to show the current temperature, but soon I hope to add a rotary encoder that will allow me to choose the current desired temperature (in theory in each room, but that’s a way off for now).

The display is one of those cheap 128 x 64 0.96″ ones that cost about £3 each from China. They are small and cheap enough to place into each wall-mounted temperature sensor, but big enough for what I want to display.

I’ve obtained the I2C version which allows me to use the same pins as the temperature sensing IC, which will – hopefully – help simplify designing the final PCB.

Code

OLED Library

To control the display, I’m using the ESP_SSD1306 library, available here: https://github.com/somhi/ESP_SSD1306 After downloading and playing around, I was able to get the display to show the current temperature everytime it’s measured. I did however have a few problems:

  • Initially, I couldn’t get anything on to show on the display. It turns out that this was due to the “Wire.begin()” function call in the ESP_SSD1306 library. It was simply using the default I2C pins, which clearly were different on my board. Simple enough to fix, simply change the above line to “Wire.begin(2, 14)” and problem solved. It would be good to be able to choose the I2C pins in the default library, so might look into upgrading the library at some point.
  • Ticker.h library and the ESP_SSD1306 don’t seem to get on well together. I was unable to figure out why, but everytime I tried to display something on the display in the middle of the Ticker function, the device would reset. I never did find a solution to this (might look into it in the near future), so instead I got rid of the Ticker library and used something else.

Finite State Machine

I always intended to add a finite state machine in this project, but though I’d be looking into it at a later date. However, due to the problems with the Ticker library, I decided to get started with it sooner rather than later.

I’ve used FMSs before in AVR programs, but not directly in the Arduino environment, but I’m still learning the “Arduino-FSM” library, available here: https://github.com/jonblack/arduino-fsm

For now the FSM is very simple and probably a bit over-kill, but later I’ll use it to allow me to set the desired temperature via a rotary encoder, and potentially add a small menu, etc.

For now, the FSM has only 1 state, 1 transition, and 1 trigger.

  • The state is called “state_waiting”, during which nothing actually happens and the device is simply waiting for something to happen – in this case for a timer to say it’s time to obtain the current temperature.
  • The trigger is called “TEMPERATURE_TIME_TRIGGER”. It’s not very catchy, but gets the job done. This trigger happens once every minute (can be easily changed) and triggers the FSM to obtain the current temperature.
  • Finally, the transition is called “transition_getting_temperature”. This transition goes and gets the current temperature, displays it on the screen, and sends it via MQTT. This transition goes from “state_waiting” back to “state_waiting”, where the FSM waits for something new to happen.

The diagram below shows the FSM in a way that should be much easier to understand.

FSM

The code has been uploaded to the Github project here: https://github.com/nerobot/Home-Automation/tree/master/ESP8266%20temperature%20sensor It’s currently quite messy and will be cleaned up soon, but for now, it might be useful to someone.

Circuit

Below, you can see a photograph of the OLED displaying the current temperature. The circuit is currently very messy, but does the job.

img_20160228_131429.jpg

Next Steps

The next step is to add the rotary encoder. I hope to get the following done soon:

  • Use the rotary encoder to choose the current desired temperature.
  • Show the desired – along with actual – temperature on the OLED.
  • Send the chosen desired temperature to node-red using MQTT, and update the shown desired temperature from node-red (again using MQTT).