Put ATmega328 in very deep sleep and listen to serial?

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.


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.