2

I am trying to get a temperature sensor (tmp36) working with an Attiny and if possible I would like it to run for several weeks. I have tried to power down the Attiny and it appears to be drawing only 0.2mA current when asleep, and around 4mA when awake (using a multimeter). However the CR2032 battery went from 3.24V to 2.9V in about 1.5 days. If it was really only using 0.2mA I would have thought it would go down much more slowly than this.

I wonder if it's using a lot of current every 8s when it turns on briefly and I'm not seeing this on the multimeter, or drawing a lot of current every hour when it reads the temperature or is there something else that I'm missing.

My circuit is an Attiny85 connected directly to a tmp36 temperature sensor. I have a capacitor connected across the GND and Vcc of the tmp36. Since I am using a surface mount tmp36 I also have the shutdown pin on the tmp36 pulled high (connected directly to Vcc). Otherwise there are no other connections, it's a very simple circuit and since the current normally appears to be 0.2mA I'm not sure why the battery voltage reading is going down so fast.

I am new to turning off the attiny(and still pretty new to arduino programming) and I copied most of the code for putting the arduino to sleep from here: https://folk.uio.no/jeanra/Microelectronics/ArduinoWatchdog.html (and possibly other places).

The code I have is as follows:

#include <stdlib.h>
#include <EEPROM.h>
#include <SoftwareSerial.h>

#include <avr/sleep.h>    // Sleep Modes
#include <avr/power.h>    // Power management
#include <avr/wdt.h>      // Watchdog timer
#include <avr/interrupt.h> // Interrupts handling

// Definitions
#define rxPin 3      
#define txPin 4

// 0 1 0 Internal 1.1V Voltage Reference, calling INTERNAL should set the            reference voltage to 1.1

SoftwareSerial mySerial(rxPin, txPin);

int currentTime = 0;
int timeLastSaveFullTemp = 0;

int nbr_remaining;
int sensorPin = 7; // This is the 7th pin on the ATtiny85, 

                         // it is an analog input when reading as digital  call it 7? analogpintochannel function 
                         // (On the HLT tutorial it is labelled as Pin2:  Analog input 1, SCK )  

float temperature = -500.00f;     // Create a new float called temperature and give it the value -500 to start with
float prevTemp = -500.00f;
float tempDiff = -500.00f;

int tempAsInt = 0;
int tempDiffAsInt = 0;  // create int to store temperature difference between samples 

signed char tempDiffAsChar; // create char to store temp difference between samples, hopefully this saves as a 1 byte no. between -128 and 127 (will store like this on EEPROM where difference is small enough since only 1 byte) (range around -128 to 128 so can't use for temp diff greater than 10 deg)
signed char warningNextDataIsTwoBytes = 111; 

int eepromAddress = 0;       // initialize variable for position in eeprom to store data collected

int decideWhatToSaveFlag = 1;    // Decide what to save,  if this = 0 then save the temp difference as a char (1 byte), otherwise save the entire temperature as an integer.


// interrupt raised by the watchdog firing
// when the watchdog fires during sleep, this function will be executed
// remember that interrupts are disabled in ISR functions
ISR(WDT_vect)
{
        // not hanging, just waiting
        // reset the watchdog
        wdt_reset();
}


// function to configure the watchdog: let it sleep 8 seconds before firing
// when firing, configure it for resuming program execution
void configure_wdt(void)
{

  cli();                           // disable interrupts for changing the registers

  MCUSR = 0;                       // reset status register flags

                               // Put timer in interrupt-only mode:                                       
  WDTCR |= 0b00011000;            // Set WDCE (5th from left) and WDE (4th from left) to enter config mode,
                               // using bitwise OR assignment (leaves other bits unchanged).
  WDTCR =  0b01000000 | 0b100001; // set WDIE: interrupt enabled, 8 seconds
                               // clr WDE: reset disabled
                               // and set delay interval (right side of bar) to 8 seconds

  sei();                           // re-enable interrupts



}

// Put arduino to sleep.
void sleep(int ncycles)
{  
 nbr_remaining = ncycles; // defines how many cycles should sleep

  // Set sleep to full power down.  Only external interrupts or
  // the watchdog timer can wake the CPU!
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);

  // Turn off the ADC while asleep.
  power_adc_disable();

  while (nbr_remaining > 0){ // while some cycles left, sleep!

  // Enable sleep and enter sleep mode.
  sleep_mode();

  // CPU is now asleep and program execution completely halts!
  // Once awake, execution will resume at this point if the
  // watchdog is configured for resume rather than restart

  // When awake, disable sleep mode
  sleep_disable();

  // we have slept one time more
  nbr_remaining = nbr_remaining - 1;

  }

  // put everything on again
  power_all_enable();

 }


// the setup routine runs once when you press reset:
void setup() {
  analogReference(INTERNAL);    // This sets the internal ref to be 1.1V (or close to this), note that this makes 1.1V the highest voltage. The TMP36 gives 1.1V at around 55 or 60 deg so need to not put the chip in temperatures hotter than 55deg
  pinMode(sensorPin, INPUT);    // make pin 7 on the ATTiny an input pin

  delay(1000);

  configure_wdt();              // configure the watchdog

  mySerial.begin(9600);         // begin software serial at rate of 9600

}

// the loop routine runs over and over forever:
void loop() {

  sleep(450);                                                 // sleep for a given number of cycles (here, 450 * 8 seconds = 60mins) in lowest power mode

  currentTime = currentTime + 60;                             // add 60 min to current time       


  if((eepromAddress<=510))                                    //  the eeprom still has space 
  {

/***********Get temperature from sensor, then calculate difference since previous reading****************/

//Earlier set the reference voltage for the analog to digital converter to 1.1V.  This means a value of 1.1V will be read as the maximum (not sure if 1023 or 1024, but anyway)
//Note that this means an input greater than 1.1V from the temperature sensor (temperature about 55deg) is bad, not sure if it will damage the circuit or give bad readings but avoid.

temperature = analogRead(sensorPin)*1100.0/1024.0;  // read value on pin 7, between 0 and 1023 where 1023 is max, convert result back to a voltage in millivolts. 
temperature = (temperature - 500)/ 10;              //Celcius temperature = [(analog voltage in mV) - 500] / 10
tempDiff = temperature - prevTemp;

/******* Change temperature diffence to integer and a signed char if it's between -2 and 2 deg ***********/
tempAsInt = int(round(temperature*10));  // multiply temp. by 10 then round and change to an integer type, this should give temp. as a three digit integer scaled by x10, e.g. 335 = 33.5deg, 120 = 12.0deg etc.
tempDiffAsInt = int (round(tempDiff*10));

// work out whether want to save complete temperature or just temp difference
if ((currentTime - timeLastSaveFullTemp) >=10)           // If the entire temperature hasn't been saved for more than 10 min, then want to save the entire temperature rather than just difference
{
  decideWhatToSaveFlag = 1;                               // Set flag to 1, since want to save entire temperature
  }
else
  decideWhatToSaveFlag = 0;                               // If the entire temperature has already been saved recently then set flag to zero since only want to save the difference.

if ((tempDiffAsInt>-20 and tempDiffAsInt<20)and (decideWhatToSaveFlag == 0))    // if temp change is less than 2 degrees and if the decideWhatToSaveFlag hasn't been previously set to 1 for another reason.
  tempDiffAsChar = (char)(tempDiffAsInt);

else decideWhatToSaveFlag = 1;                                                  // if the temp diff since prev sample is > 2 degrees or less than -2 degrees then set flag to 1 since want to save the entire temp. not just the difference


if (decideWhatToSaveFlag == 0) 
  {
  //mySerial.print(" Temp diff");
  EEPROM.put(eepromAddress, tempDiffAsChar);
  eepromAddress = eepromAddress + sizeof(tempDiffAsChar);
  }

else 
  {
  EEPROM.put(eepromAddress, warningNextDataIsTwoBytes);
  eepromAddress = eepromAddress + sizeof(warningNextDataIsTwoBytes);
  EEPROM.put(eepromAddress, tempAsInt);
  eepromAddress = eepromAddress + sizeof(tempAsInt);
  timeLastSaveFullTemp = currentTime;                       // Update time entire temperature was last saved to the current time.   
  }

prevTemp = temperature; // copy temp to prev temp for next time.

}

}

Please excuse my coding I'm still learning. I deleted my comments at the start of the program which explain why I'm changing the temp. to char etc. as they weren't really relevant to this question but if anyone wants them let me know.

Here is the circuit, a 3V battery is connected between Vcc and GND. The capacitor is 0.1uF ceramic. I am using the SOIC (surfacemount) type package of the tmp36, the (not) shutdown pin in the SOIC version needs to be pulled high so I have connected it to Vcc as specified in the datasheet (this pin doesn't exist in the TO92 through-hole version of the tmp36). schematic

All help is greatly appreciated,

I'm also interested in suggestions for reducing the current further. (Or for how to store more temperatures in a 512 byte EEPROM but that's less important than the battery)

Thanks very much!!

p.s. I have tested this code and it did save one temperature per hour as expected. (The section of code that saves temperature differences rather than temperature doesn't run when there is a 60 min gap between temperatures so all temperatures were saved to the eeprom as integers).

7
  • 0.2mA is noise to most DMMs. What are you using to measure it? Mar 21, 2018 at 6:34
  • I tried one $5 DMM and another slightly better one but brand Jaytech so not really particularly accurate probably, but I would have thought if the current was 1mA or more it would say so. Unless it's oscillating up and down quickly or something.
    – Shara
    Mar 21, 2018 at 6:50
  • Going to try changing the clock speed to 1MHz (from 8MHz) to see if that helps...
    – Shara
    Mar 21, 2018 at 6:53
  • I uploaded your code to my Attiny85 (nothing connected to it) and got 0.2 mA current draw at 3V. That actually sounds far too high for sleeping, it should be more like 6µA rather than 200µA.
    – Nick Gammon
    Mar 21, 2018 at 7:09
  • @IgnacioVazquez-Abrams Measurement error does not seem to apply in this particular case. Possibly other things in the (unposted) circuit are drawing more than expected.
    – Nick Gammon
    Mar 21, 2018 at 7:22

2 Answers 2

5

2.9V doesn't sound too bad. See a datasheet for the CR2032 I found:

CR2032 datasheet

It looks like slightly more than 2.9V would be the expected voltage for up to 600 hours.

3.2V looks like it is fully charged, and won't stay at that level for long.

Having said that, 200 µA current sounds like a lot. You should be able to get it down to 6 µA. Perhaps post your schematic?


After a bit of experimenting I got the current draw down to the (expected) lower level of 4.25µA running at 3V, and 6.77µA running at 5V.

You had not in fact turned the ADC off. You need to change:

 // Turn off the ADC while asleep.
  power_adc_disable();

To:

  // Turn off the ADC while asleep.
  ADCSRA = 0;            // turn off ADC
  power_all_disable();

I can't comment on what extra draw, if any, your TMP36 will cause, however this should help get the overall current down.


Of course, you need to put the ADC back on afterwards, so this might be better:

  // Turn off the ADC while asleep.
  byte saved_ADCSRA = ADCSRA;
  ADCSRA = 0;            // turn off ADC
  power_all_disable();

  ... sleep here

  power_all_enable();
  ADCSRA = saved_ADCSRA;
10
  • oh, thanks!! This looks like the problem, it looks like I need to test it for more than 1.5 days then. I did notice it was already 3.14 after an hour or so. I will try testing it for a few days and then accept your answer assuming that was the problem (which it seems like it is). I hope so :) I just tried changing the frequency to 1MHz (from 8MHz) which lowered the ON current and sleep current remained about the same. Will post circuit, I think perhaps I can turn off the tmp36 but haven't tried yet, was a bit worried it might not turn on again or something.
    – Shara
    Mar 21, 2018 at 7:45
  • ...also it's possible it's not using 0.2mA as I don't have a high accuracy multimeter.
    – Shara
    Mar 21, 2018 at 7:49
  • I will set up a few circuits and run them for a week and see how long they last :), then get back to you, thanks again!! (I need to wait until tomorrow to start, unfortunately all the shops which potentially sell batteries in the small town where I Iive at the moment have already closed for today).
    – Shara
    Mar 21, 2018 at 8:36
  • p.s. thanks again, I also read your webpage about how to lower current draw a month or so ago and it really helped a lot, otherwise probably wouldn't have gotten this far even.
    – Shara
    Mar 21, 2018 at 8:41
  • In my amended reply I show how to get the current draw down to 4.25µA (excluding whatever the TMP36 takes).
    – Nick Gammon
    Mar 21, 2018 at 20:03
2

THIS IS NOT AN ANSWER; it's just a very long comment and I was not able to add all the formatting and ideas in the comment I was writing. Please don't consider it as a valid answer.

I leave all the low current topics to Nick, since he's doing a great work as usual. I wanted just to give you some ideas for the temperature acquisition and storing.

First of all, the floats. Forget them. floats are one of the worst things you can do to your poor microcontroller, unless you have an onboard FPU.

I haven't checked your sensor, so I assume that the calculations you performed were correct. (Note: voltages above 1.1V should not damage the board, but will not be detected - the value will cap at 1023)

So when you read number X (from 0 to 1023) you get the temperature by writing

((X * 1100 / 1024) - 500) / 10

You are using floats to do this. I don't like them at all, so let's switch to ints. Moreover you decided to store them with a 0.1°C precision, so that /10 is meaningless. So in the end

<integral type> finalTempInTenthsOfDegree = ((X * 1100 / 1024) - 500);

Now, the types. The value of the final temperature is between +60°C (corresponding to 1.1V) and -50°C (0V - not sure about this, but let's stick with these value). This means an integral value going from +600 to -500; the proper type is therefore int16_t (signed 2-bytes value). During your calculations, the X (which is a uint16_t) is multiplied by 1100; this value can rise up to 1125300 (when X = 1023), which does not fit a 2-bytes variable. You can reduce 1100/1024 to 275/256, but this does not solve the problem.

EDIT: As Edgar Bonet pointed out in the comments, the problem of having a 4 bytes is not solved with this trick, but dividing by 1024 is more "time consuming" than dividing by 256 (because the 10-bit shift involves shifting bits through bytes, while a 8-bit shift is just moving the bytes. His tests (avr-gcc 4.9.2, optimization for size -Os) show that a shift by 10 bits (=division by 10) is converted in a routine to shift one bit through all the four bytes and then this is repeated 10 times. This lead to 72 clock cycles. A 8-bit shift, on the other hand, was converted to a byte movement for three out of four bytes and the zeroing of the uppermost. So 4 clock cycles. So... Yes, this does not solve the problem, but can optimize the code; I'll think of this trick in my future projects. Of course this can be done in a much more optimized way, and the number of cycles required can change according to the way the operation is written and the optimization level, but it is worth remembering that if you shift by a multiple of 8 bits (on an 8-bit machine) it is more optimized than another arbitrary value.

In any case, even with either of the two values, a 4-bytes int is required. In order to reduce unnecessary overhead, let's switch it back to a 2-bytes whenever possible. Something like

int16_t finalTemp = ((int16_t)((((uint32_t)X) * 1100U) / 1024) - 500;

So the operation order is

  1. Take the X value
  2. Promote it to a unsigned 32 bit variable
  3. Multiplicate by 1100 (values from 1125300 to 0)
  4. Divide it by 1024 (values from 1098 to 0)
  5. Cast it back to a signed 16 bit value
  6. subtract 500 (values from 598 to -500)

Now, the storage. If you store the values as they are, you will get 256 measurements, which at one measurement per hour mean something like 10 days.

One easy way to increase this space is noticing that you are using only 11 bytes out of 16. So you can compact it and get 372 measurements (around 15 days)

The trick you used (ten measurements with just the difference) is ok only if you guarantee that in ten hours the value will not drift by more than +/- 12°C. This is not guaranteed and probably also not optimal. I'd prefer another approach, which is based on the fact that you are using only 11 bytes out of 16. You can have two different data stored in the EEPROM, which are differences (1 byte) or whole temperatures (2 bytes). You will distinguish them with the first bits: a 0 denotes a difference, a 1 a whole temperature (followed by 0 is the low byte, by a 1 is a high byte). This means that for the difference you have seven bits, so it can range from +63 to -64. Whenever you reach a difference from the previous temperature within 6 degrees you don't save anymore the whole temperature. The code to do this is for instance the following one:

// int16_t finalTemp calculated before
// int16_t prevTemp initialised, at beginning, for instance to -32000, so the difference is greater and you force a full writing at the first round

int16_t tempDiff = finalTemp - prevTemp;
prevTemp = finalTemp; // update the temperature

if ((tempDiff <= -63) || (tempDiff >= 63)
{
    // In this case we want to save the whole integer
    EEPROM.update(eepromAddress++, (((uint8_t)finalTemp) & 0x3F) | 0x80); // Write the lowest 6 bits, and set the first two to 0b10
    EEPROM.update(eepromAddress++, (((uint8_t)(finalTemp >> 6)) & 0x3F)| 0xC0); // Write the highest 6 bits, and set the first two to 0b11
}
else
{
    // In this case we save only the difference
    EEPROM.update(eepromAddress++, ((uint8_t)tempDiff) & 0x7F); // Just remove the highest bit, and store it
}

In this case, provided that the temperature does not vary a lot, you will get 511 measurements, which is like 21 days. For each variation greater than 6°C/h you will lose one hour of measurement, down to 256 measurements. Please note that I wrote EEPROM.update instead of write, even if this is not necessary (just one of the things I do every time).

The last possibility is to switch to a 0.5°C resolution. If you use this resolution, you can map your values on a range 120/-100, thus being able to use only one byte for each measurement. Since the accuracy is +/- 2°C, you can also choose this without problems.

Apart from the storage, if you record only 256 bytes of information how do you know which are the correct values and which are the previous ones? My suggestion is to reserve one value (for instance if you have used my code you can get an impossible value such as 0x40 - which if I'm not wrong maps to DIFFERENCE -64, which is impossible since the difference is stored only when greater than -63) and fill the memory before starting to insert the values. beware that if you do it then turning on the board will reset the memory...

One last suggestion: you have a sensor which stores the data internally, but.. How do you get them out of it? My personal suggestion is to have a dedicated pin (for instance PB3) tied to a jumper. If it is open then you will proceed with the normal program, otherwise you wait for a download request. (don't do the opposite since the jumper inserted drains the battery). Consequently the code can be something like this:

const uint8_t invalidEepromValue = 0x40;
const int16_t maxAddress = 510; // Not equal to 512 to avoid a last reading of 2 bytes

void dumpMemory()
{
    int16_t prevTemp = 0;
    for (int16_t i = 0; i < maxAddress; i++)
    {
        int16_t value = EEPROM.read(i);
        if (value == invalidEepromValue)
            break; // Finished the reading

        if (value & 0x80)
        { // WholeData
            int16_t value2 = EEPROM.read(i+1);
            if (((((uint8_t)value) & 0xC0) != 0x80) || ((((uint8_t)value2) & 0xC0) != 0xC0))
            {
                mySerial.print("Error with memory cell ");
                mySerial.println(i);
                continue;
            }

            prevTemp = (value & 0x3F) | ((value2 & 0x3F) << 6);
            if (value2 & 0x40) prevTemp |= 0xF000; // Extend the sign

            mySerial.println(prevTemp);
            i++; // skip the next value (already analysed)
        }
        else
        { // Difference
            if (value & 0x40) value |= 0xFF80; // Extend the sign

            prevTemp += value;
            mySerial.println(prevTemp);
        }
    }
}

void setup() {
    pinMode(modePin, INPUT_PULLUP);
    mySerial.begin(9600);         // begin software serial at rate of 9600

    delay(1);
    if (digitalRead(modePin) == LOW)
    { // Enter read mode
        while (1) // loop forever
        {
            while (mySerial.available() == 0); // Wait for some chars

            dumpMemory();

            while (mySerial.available() > 0)
                mySerial.read(); // Flush receiving buffer
        }
    }

    // Clear the memory
    for (int16_t i = 0; i <= maxAddress; i++) // Equal to cover the last reading of 2 bytes
        EEPROM.update(i, invalidEepromValue); // Here update is almost mandatory

    // Go on with the initialization you already implemented
    ...
}

I hope that the software serial works even without the main structure (so in a while(1) in the setup).

Please keep in mind that all the code written here has NOT been tested, so it may require some corrections

9
  • 1
    You wrote: “You can reduce 1100/1024 to 275/256, but this does not solve the problem.” It may not solve the problem, but it can be used as an optimization. Dividing an unsigned number by 256 is almost free, as it only involves moving bytes. Dividing by 1024, on the other hand, requires extra shift instructions. Mar 21, 2018 at 21:55
  • Thank you very much, I really appreciate the time taken to help. I will have a look at the current first and once that's working have a look at this. Actually my problem is a bit bigger than 1 point per hour. I plan to measure skin temperature of bats and ideally it would be good to have one temperature per 10 minutes for 10 days or longer. Since with 512 bytes I'm not sure if that's possible I'm thinking what would be best, I really only need to know if the bats lower their body temperature (use torpor) so perhaps I will do one point per hour and then....
    – Shara
    Mar 22, 2018 at 5:28
  • increase the frequency to 10 minutes if the temperature changes. Though of course any time I change the frequency I also need to record the time so I can work out at what time each temperature was recorded. Overall therefore a better option might be to put up with a lower sample rate such as one point per half hour. It could be enough, I will have to test it and see which one is best for the information I'm trying to record.
    – Shara
    Mar 22, 2018 at 5:36
  • Anyway, thank you very much, I will definitely look at this properly and probably use it, have upvoted it. Sure it will also be useful to anyone else doing similar things. Thanks also about the float comments, good point, I don't really need them at all!
    – Shara
    Mar 22, 2018 at 5:39
  • 1
    I just gave it a try with avr-gcc 4.9.2 at -Os. The division by 1024 compiles to a loop of 10 iterations, each shifting the 4 bytes of the number one by one. With the loop overhead, the total execution time is 72 CPU cycles. The division by 256 compiles to an instruction that clears one byte, and three register moves. Total execution time is 4 CPU cycles. Both codes are sub-optimal. By reassigning registers, the division by 1024 could be done in 6 cycles and the division by 256 in 0 cycles. Mar 22, 2018 at 8:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.