13

I have investigated the sleeps options of the ATmega328, and read a few articles about it, and I would like to understand if there are more options.

So I would like to get as low current as possible, so that anything less that 100uA would be good - as long as I can listen to uart and interrupts for waking up.

I am using a custom PCB (not the UNO), with ATmega328p.

Setting the chip to deep sleep:

 set_sleep_mode (SLEEP_MODE_PWR_DOWN);  
 sleep_enable();
 sleep_cpu ();

would not wake it up with serial communication,according to this.

You will need to put it in IDLE mode, to listen to serial, but this would consume a few mA -bad.

I have found this link where you can connect in hardware the serial to the interrupt - which is dangerous so you can loose data, and moreover, I need these 2 interrupts pins.

I also read this article of Gammon, where you can disable some things, so you can get IDLE sleep with much lower power - but he didn't mention how exactly you get from this:

 power_adc_disable();
      power_spi_disable();
      power_timer0_disable();
      power_timer1_disable();
      power_timer2_disable();
      power_twi_disable();

So, bottom line, is there any option out there, to get less than 0.25mA at least, and also listen to serial port, without any hardware manipulation? For example, waking up with long serial data input?

  • 1
    @NickAlexeev this is an ATmega328 question not an Arduino one as it is deals directly with the chip far below the level of Arduino. Stop with the improper migrations already! – Chris Stratton Jul 5 '15 at 16:29
  • 1
    Hardly. Wanting to wake an Arduino from sleep can't really be dismissed because it has an ATmega328 chip in it. At that rate you'll be able to bounce all the question about Arduinos back to the EE site. – Nick Gammon Jul 9 '15 at 7:43
11

A board we make does this.

  • The RX pin is wired to INT0
  • INT0 pin set to input or input pullup depending on how the RX line is driven
  • On sleep, INT0 low level interrupt is enabled

    //Clear software flag for rx interrupt
    rx_interrupt_flag = 0;
    //Clear hardware flag for rx interrupt
    EIFR = _BV(INTF0);
    //Re-attach interrupt 0
    attachInterrupt(INT_RX, rx_interrupt, HIGH);
    
  • INT0 interrupt service routine sets a flag and disables the interrupt

    void rx_interrupt()
    {
        detachInterrupt(INT_RX);
        rx_interrupt_flag = 1;
    }
    
  • On wakeup, we check for the flag (there are other interrupt sources)

On the comms side of things we use a message protocol that has a start character > and end character \r. e.g. >setrtc,2015,07,05,20,58,09\r. This give some basic protection against losing messages, as incoming characters are not processed until a > is received. To wake up the device we send a dummy message before transmission. A single character would do it, but we send >wakeup\r hehe.

The device stays awake for 30 seconds after the last message is received in case of new messages. If a new message is receive the 30 second timer is reset. The PC interface software sends a dummy message every second to keep the device awake while the user has it connected for configuration etc.

This method gives absolutely no problems at all. The board with a few peripherals uses about 40uA when sleeping. The actual current consumed by the ATMega328P is probably around 4uA.

Update

At look at the datasheet shows that the RX pin is also pin change interrupt pin 16 (PCINT16)

Thus another method without wires may be

  • Before sleep: Set the port change interrupt mask bit in PCMSK2 for PCINT16, clear the pin change port 2 flag in PCIFR, enable the pin change port 2 interrupt (PCINT16-PCINT23) by setting PCIE2 in PCICR.

  • Setup an ISR for the pin change port 2 interrupt and continue as before.

The only caveat with the port change interrupt is that the interrupt is shared across all the 8 pins that are enabled for that port. So if you have more than one pin change enabled for the port, you have to determine which triggered the interrupt in the ISR. This is not a problem if you are not using any other pin change interrupts on that port (PCINT16-PCINT23 in this case)

Ideally this is how I would have designed our board but what we have works.

  • Thanks very much . Is there no other way than hardware tricks??? So you only connect rx to int0/int1 with 1 line ?? – Curnelious Jul 5 '15 at 11:14
  • 1
    Actually I just had a look at the data sheet and you might be able to use a pin change interrupt – geometrikal Jul 5 '15 at 11:19
  • Thanks,what would be the different? Anyway i would have to wake up with rx on int1? – Curnelious Jul 5 '15 at 11:20
  • You only need 1 interrupt pin. I posted some more above - you could use the RX pin as a pin change interrupt. I haven't done this though so there might be a few catches like maybe you will have to disable RX / enable pin change before sleep and disable pin change / enable RX after wakeup – geometrikal Jul 5 '15 at 11:31
  • thanks, i am not sure why should be a problem with just connecting rx to INT1,set the to interrupt on high ,than disable interrupts when int1 happens, and enabling them again when go to sleep ? – Curnelious Jul 5 '15 at 11:34
7

The code below achieves what you are asking:

#include <avr/sleep.h>
#include <avr/power.h>

const byte AWAKE_LED = 8;
const byte GREEN_LED = 9;
const unsigned long WAIT_TIME = 5000;

ISR (PCINT2_vect)
{
  // handle pin change interrupt for D0 to D7 here
}  // end of PCINT2_vect

void setup() 
{
  pinMode (GREEN_LED, OUTPUT);
  pinMode (AWAKE_LED, OUTPUT);
  digitalWrite (AWAKE_LED, HIGH);
  Serial.begin (9600);
} // end of setup

unsigned long lastSleep;

void loop() 
{
  if (millis () - lastSleep >= WAIT_TIME)
  {
    lastSleep = millis ();

    noInterrupts ();

    byte old_ADCSRA = ADCSRA;
    // disable ADC
    ADCSRA = 0;  
    // pin change interrupt (example for D0)
    PCMSK2 |= bit (PCINT16); // want pin 0
    PCIFR  |= bit (PCIF2);   // clear any outstanding interrupts
    PCICR  |= bit (PCIE2);   // enable pin change interrupts for D0 to D7

    set_sleep_mode (SLEEP_MODE_PWR_DOWN);  
    power_adc_disable();
    power_spi_disable();
    power_timer0_disable();
    power_timer1_disable();
    power_timer2_disable();
    power_twi_disable();

    UCSR0B &= ~bit (RXEN0);  // disable receiver
    UCSR0B &= ~bit (TXEN0);  // disable transmitter

    sleep_enable();
    digitalWrite (AWAKE_LED, LOW);
    interrupts ();
    sleep_cpu ();      
    digitalWrite (AWAKE_LED, HIGH);
    sleep_disable();
    power_all_enable();

    ADCSRA = old_ADCSRA;
    PCICR  &= ~bit (PCIE2);   // disable pin change interrupts for D0 to D7
    UCSR0B |= bit (RXEN0);  // enable receiver
    UCSR0B |= bit (TXEN0);  // enable transmitter
  }  // end of time to sleep

  if (Serial.available () > 0)
  {
    byte flashes = Serial.read () - '0';
    if (flashes > 0 && flashes < 10)
      {
      // flash LED x times 
      for (byte i = 0; i < flashes; i++)
        {
        digitalWrite (GREEN_LED, HIGH);
        delay (200);  
        digitalWrite (GREEN_LED, LOW);
        delay (200);  
        }
      }        
  }  // end of if

}  // end of loop

I used a pin-change interrupt on the Rx pin to notice when serial data arrives. In this test the board goes to sleep if there is no activity after 5 seconds (the "awake" LED goes out). Incoming serial data causes the pin-change interrupt to wake the board. It looks for a number and flashes the "green" LED that number of times.

Measured current

Running at 5 V, I measured about 120 nA of current when asleep (0.120 µA).

Awakening message

A problem however is that the first arriving byte is lost due to the fact that the serial hardware expects a falling level on Rx (the start bit) which has already arrived by the time it is fully awake.

I suggest (as in geometrikal's answer) that you first send an "awake" message, and then pause for a short time. The pause is to make sure the hardware does not interpret the next byte as part of the awake message. After that it should work fine.


Since this uses a pin-change interrupt no other hardware is required.


Amended version using SoftwareSerial

The version below successfully processes the first byte received on serial. It does this by:

  • Using SoftwareSerial which uses pin-change interrupts. The interrupt which is caused by the start bit of the first serial byte also wakes the processor.

  • Setting the fuses so that we use:

    • Internal RC oscillator
    • BOD disabled
    • The fuses were: Low: 0xD2, High: 0xDF, Extended: 0xFF

Inspired by FarO in a comment, this lets the processor wake up in 6 clock cycles (750 ns). At 9600 baud each bit time is 1/9600 (104.2 µs) so the extra delay is insignificant.

#include <avr/sleep.h>
#include <avr/power.h>
#include <SoftwareSerial.h>

const byte AWAKE_LED = 8;
const byte GREEN_LED = 9;
const unsigned long WAIT_TIME = 5000;
const byte RX_PIN = 4;
const byte TX_PIN = 5;

SoftwareSerial mySerial(RX_PIN, TX_PIN); // RX, TX

void setup() 
{
  pinMode (GREEN_LED, OUTPUT);
  pinMode (AWAKE_LED, OUTPUT);
  digitalWrite (AWAKE_LED, HIGH);
  mySerial.begin(9600);
} // end of setup

unsigned long lastSleep;

void loop() 
{
  if (millis () - lastSleep >= WAIT_TIME)
  {
    lastSleep = millis ();

    noInterrupts ();

    byte old_ADCSRA = ADCSRA;
    // disable ADC
    ADCSRA = 0;  

    set_sleep_mode (SLEEP_MODE_PWR_DOWN);  
    power_adc_disable();
    power_spi_disable();
    power_timer0_disable();
    power_timer1_disable();
    power_timer2_disable();
    power_twi_disable();

    sleep_enable();
    digitalWrite (AWAKE_LED, LOW);
    interrupts ();
    sleep_cpu ();      
    digitalWrite (AWAKE_LED, HIGH);
    sleep_disable();
    power_all_enable();

    ADCSRA = old_ADCSRA;
  }  // end of time to sleep

  if (mySerial.available () > 0)
  {
    byte flashes = mySerial.read () - '0';
    if (flashes > 0 && flashes < 10)
      {
      // flash LED x times 
      for (byte i = 0; i < flashes; i++)
        {
        digitalWrite (GREEN_LED, HIGH);
        delay (200);  
        digitalWrite (GREEN_LED, LOW);
        delay (200);  
        }
      }        
  }  // end of if

}  // end of loop

Power consumption when asleep was measured as 260 nA (0.260 µA) so that is very low consumption when not needed.

Note that with the fuses set like that, the processor runs at 8 MHz. Thus you need to tell the IDE about that (eg. select "Lilypad" as the board type). That way the delays and SoftwareSerial will work at the correct speed.

  • @NickGammon thanks a lot ! i did it already and it worked . Is that way is common in other products that we use everyday, or they have other ways to listen to comm and sleep ? (all MCU's cant listen to uart at deep sleep?) – Curnelious Jul 6 '15 at 8:20
  • I was reading the datasheet and it states that when using internal oscillator, only 14 clock cycles are needed to start the chip, provided that BOD is used. If the power source is always up (batteries), that could be used also without BOD? violating the specs of course. That would bring the chip up very shortly after the incoming UART edge, but still I'm not sure it would be enough to catch the first byte. – FarO Nov 21 '17 at 16:01
  • Yes, 14 clock cycles isn't long, however possibly the UART would still miss the edge (after all, the edge is when the processor notices the change). So even if it starts up very soon after the edge it might still miss it. – Nick Gammon Nov 22 '17 at 0:37
  • A bit of testing indicates that (even with BOD enabled) it doesn't work. The processor needs to be awake to notice the leading edge (start bit) and thus powering it up after receiving it (even if very shortly after) doesn't work. – Nick Gammon Nov 22 '17 at 2:15
  • The 14 clock cycles are after reset. You only need 6 cycles after power-down, if you use the internal RC oscillator. See additional example code. – Nick Gammon Nov 23 '17 at 0:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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